Compare commits
	
		
			7 Commits
		
	
	
		
			5e9fe86cd6
			...
			a5f864b802
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a5f864b802 | |||
| 153cc450d9 | |||
| 457bf09b52 | |||
| 43f56b283c | |||
| 39aa0eeea4 | |||
| 8046fec794 | |||
| fd282a6595 | 
| @@ -12,7 +12,7 @@ Copy the share/solarfm folder to your user .config/ directory too. | |||||||
|  |  | ||||||
| <h6>Install Setup</h6> | <h6>Install Setup</h6> | ||||||
| ``` | ``` | ||||||
| sudo apt-get install python3.8 python3-setproctitle python3-gi wget ffmpegthumbnailer steamcmd | sudo apt-get install xclip python3.8 python3-setproctitle python3-gi wget ffmpegthumbnailer steamcmd | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| # Known Issues | # Known Issues | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								plugins/movie_tv_info/tmdbscraper/lib/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								plugins/movie_tv_info/tmdbscraper/lib/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | """ | ||||||
|  |     Pligin Module | ||||||
|  | """ | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  |  | ||||||
|  | def get_imdb_id(uniqueids): | ||||||
|  |     imdb_id = uniqueids.get('imdb') | ||||||
|  |     if not imdb_id or not imdb_id.startswith('tt'): | ||||||
|  |         return None | ||||||
|  |     return imdb_id | ||||||
|  |  | ||||||
|  | # example format for scraper results | ||||||
|  | _ScraperResults = { | ||||||
|  |     'info', | ||||||
|  |     'ratings', | ||||||
|  |     'uniqueids', | ||||||
|  |     'cast', | ||||||
|  |     'available_art', | ||||||
|  |     'error', | ||||||
|  |     'warning' # not handled | ||||||
|  | } | ||||||
| @@ -0,0 +1,75 @@ | |||||||
|  | # coding: utf-8 | ||||||
|  | # | ||||||
|  | # Copyright (C) 2020, Team Kodi | ||||||
|  | # | ||||||
|  | # This program is free software: you can redistribute it and/or modify | ||||||
|  | # it under the terms of the GNU General Public License as published by | ||||||
|  | # the Free Software Foundation, either version 3 of the License, or | ||||||
|  | # (at your option) any later version. | ||||||
|  | # | ||||||
|  | # This program is distributed in the hope that it will be useful, | ||||||
|  | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | # GNU General Public License for more details. | ||||||
|  | # | ||||||
|  | # You should have received a copy of the GNU General Public License | ||||||
|  | # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
|  | """Functions to interact with various web site APIs.""" | ||||||
|  |  | ||||||
|  | from __future__ import absolute_import, unicode_literals | ||||||
|  |  | ||||||
|  | import json | ||||||
|  | # from pprint import pformat | ||||||
|  | try: #PY2 / PY3 | ||||||
|  |     from urllib2 import Request, urlopen | ||||||
|  |     from urllib2 import URLError | ||||||
|  |     from urllib import urlencode | ||||||
|  | except ImportError: | ||||||
|  |     from urllib.request import Request, urlopen | ||||||
|  |     from urllib.error import URLError | ||||||
|  |     from urllib.parse import urlencode | ||||||
|  | try: | ||||||
|  |     from typing import Text, Optional, Union, List, Dict, Any  # pylint: disable=unused-import | ||||||
|  |     InfoType = Dict[Text, Any]  # pylint: disable=invalid-name | ||||||
|  | except ImportError: | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  | HEADERS = {} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def set_headers(headers): | ||||||
|  |     HEADERS.update(headers) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def load_info(url, params=None, default=None, resp_type = 'json'): | ||||||
|  |     # type: (Text, Optional[Dict[Text, Union[Text, List[Text]]]]) -> Union[dict, list] | ||||||
|  |     """ | ||||||
|  |     Load info from external api | ||||||
|  |  | ||||||
|  |     :param url: API endpoint URL | ||||||
|  |     :param params: URL query params | ||||||
|  |     :default: object to return if there is an error | ||||||
|  |     :resp_type: what to return to the calling function | ||||||
|  |     :return: API response or default on error | ||||||
|  |     """ | ||||||
|  |     theerror = '' | ||||||
|  |     if params: | ||||||
|  |         url = url + '?' + urlencode(params) | ||||||
|  |     req = Request(url, headers=HEADERS) | ||||||
|  |     try: | ||||||
|  |         response = urlopen(req) | ||||||
|  |     except URLError as e: | ||||||
|  |         if hasattr(e, 'reason'): | ||||||
|  |             theerror = {'error': 'failed to reach the remote site\nReason: {}'.format(e.reason)} | ||||||
|  |         elif hasattr(e, 'code'): | ||||||
|  |             theerror = {'error': 'remote site unable to fulfill the request\nError code: {}'.format(e.code)} | ||||||
|  |         if default is not None: | ||||||
|  |             return default | ||||||
|  |         else: | ||||||
|  |             return theerror | ||||||
|  |     if resp_type.lower() == 'json': | ||||||
|  |         resp = json.loads(response.read().decode('utf-8')) | ||||||
|  |     else: | ||||||
|  |         resp = response.read().decode('utf-8') | ||||||
|  |     return resp | ||||||
| @@ -0,0 +1,87 @@ | |||||||
|  | from . import api_utils | ||||||
|  | try: | ||||||
|  |     from urllib import quote | ||||||
|  | except ImportError: # py2 / py3 | ||||||
|  |     from urllib.parse import quote | ||||||
|  |  | ||||||
|  | API_KEY = '384afe262ee0962545a752ff340e3ce4' | ||||||
|  | API_URL = 'https://webservice.fanart.tv/v3/movies/{}' | ||||||
|  |  | ||||||
|  | ARTMAP = { | ||||||
|  |     'movielogo': 'clearlogo', | ||||||
|  |     'hdmovielogo': 'clearlogo', | ||||||
|  |     'hdmovieclearart': 'clearart', | ||||||
|  |     'movieart': 'clearart', | ||||||
|  |     'moviedisc': 'discart', | ||||||
|  |     'moviebanner': 'banner', | ||||||
|  |     'moviethumb': 'landscape', | ||||||
|  |     'moviebackground': 'fanart', | ||||||
|  |     'movieposter': 'poster' | ||||||
|  | } | ||||||
|  |  | ||||||
|  | def get_details(uniqueids, clientkey, language, set_tmdbid): | ||||||
|  |     media_id = _get_mediaid(uniqueids) | ||||||
|  |     if not media_id: | ||||||
|  |         return {} | ||||||
|  |  | ||||||
|  |     movie_data = _get_data(media_id, clientkey) | ||||||
|  |     movieset_data = _get_data(set_tmdbid, clientkey) | ||||||
|  |     if not movie_data and not movieset_data: | ||||||
|  |         return {} | ||||||
|  |  | ||||||
|  |     movie_art = {} | ||||||
|  |     movieset_art = {} | ||||||
|  |     if movie_data: | ||||||
|  |         movie_art = _parse_data(movie_data, language) | ||||||
|  |     if movieset_data: | ||||||
|  |         movieset_art = _parse_data(movieset_data, language) | ||||||
|  |         movieset_art = {'set.' + key: value for key, value in movieset_art.items()} | ||||||
|  |  | ||||||
|  |     available_art = movie_art | ||||||
|  |     available_art.update(movieset_art) | ||||||
|  |  | ||||||
|  |     return {'available_art': available_art} | ||||||
|  |  | ||||||
|  | def _get_mediaid(uniqueids): | ||||||
|  |     for source in ('tmdb', 'imdb', 'unknown'): | ||||||
|  |         if source in uniqueids: | ||||||
|  |             return uniqueids[source] | ||||||
|  |  | ||||||
|  | def _get_data(media_id, clientkey): | ||||||
|  |     headers = {'api-key': API_KEY} | ||||||
|  |     if clientkey: | ||||||
|  |         headers['client-key'] = clientkey | ||||||
|  |     api_utils.set_headers(headers) | ||||||
|  |     fanarttv_url = API_URL.format(media_id) | ||||||
|  |     return api_utils.load_info(fanarttv_url, default={}) | ||||||
|  |  | ||||||
|  | def _parse_data(data, language): | ||||||
|  |     result = {} | ||||||
|  |     for arttype, artlist in data.items(): | ||||||
|  |         if arttype not in ARTMAP: | ||||||
|  |             continue | ||||||
|  |         for image in artlist: | ||||||
|  |             image_lang = _get_imagelanguage(arttype, image) | ||||||
|  |             if image_lang and image_lang != language: | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |             generaltype = ARTMAP[arttype] | ||||||
|  |             if generaltype == 'poster' and not image_lang: | ||||||
|  |                 generaltype = 'keyart' | ||||||
|  |             if artlist and generaltype not in result: | ||||||
|  |                 result[generaltype] = [] | ||||||
|  |  | ||||||
|  |             url = quote(image['url'], safe="%/:=&?~#+!$,;'@()*[]") | ||||||
|  |             resultimage = {'url': url, 'preview': url.replace('.fanart.tv/fanart/', '.fanart.tv/preview/')} | ||||||
|  |             result[generaltype].append(resultimage) | ||||||
|  |  | ||||||
|  |     return result | ||||||
|  |  | ||||||
|  | def _get_imagelanguage(arttype, image): | ||||||
|  |     if 'lang' not in image or arttype == 'moviebackground': | ||||||
|  |         return None | ||||||
|  |     if arttype in ('movielogo', 'hdmovielogo', 'hdmovieclearart', 'movieart', 'moviebanner', | ||||||
|  |             'moviethumb', 'moviedisc'): | ||||||
|  |         return image['lang'] if image['lang'] not in ('', '00') else 'en' | ||||||
|  |     # movieposter may or may not have a title and thus need a language | ||||||
|  |     return image['lang'] if image['lang'] not in ('', '00') else None | ||||||
| @@ -0,0 +1,72 @@ | |||||||
|  | # -*- coding: UTF-8 -*- | ||||||
|  | # | ||||||
|  | # Copyright (C) 2020, Team Kodi | ||||||
|  | # | ||||||
|  | # This program is free software: you can redistribute it and/or modify | ||||||
|  | # it under the terms of the GNU General Public License as published by | ||||||
|  | # the Free Software Foundation, either version 3 of the License, or | ||||||
|  | # (at your option) any later version. | ||||||
|  | # | ||||||
|  | # This program is distributed in the hope that it will be useful, | ||||||
|  | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | # GNU General Public License for more details. | ||||||
|  | # | ||||||
|  | # You should have received a copy of the GNU General Public License | ||||||
|  | # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
|  | # | ||||||
|  | # IMDb ratings based on code in metadata.themoviedb.org.python by Team Kodi | ||||||
|  | # pylint: disable=missing-docstring | ||||||
|  |  | ||||||
|  | import re | ||||||
|  | from . import api_utils | ||||||
|  | from . import get_imdb_id | ||||||
|  |  | ||||||
|  | IMDB_RATINGS_URL = 'https://www.imdb.com/title/{}/' | ||||||
|  | IMDB_RATING_REGEX = re.compile(r'itemprop="ratingValue".*?>.*?([\d.]+).*?<') | ||||||
|  | IMDB_VOTES_REGEX = re.compile(r'itemprop="ratingCount".*?>.*?([\d,]+).*?<') | ||||||
|  | IMDB_TOP250_REGEX = re.compile(r'Top Rated Movies #(\d+)') | ||||||
|  |  | ||||||
|  | def get_details(uniqueids): | ||||||
|  |     imdb_id = get_imdb_id(uniqueids) | ||||||
|  |     if not imdb_id: | ||||||
|  |         return {} | ||||||
|  |     votes, rating, top250 = _get_ratinginfo(imdb_id) | ||||||
|  |     return _assemble_imdb_result(votes, rating, top250) | ||||||
|  |  | ||||||
|  | def _get_ratinginfo(imdb_id): | ||||||
|  |     response = api_utils.load_info(IMDB_RATINGS_URL.format(imdb_id), default = '', resp_type='text') | ||||||
|  |     return _parse_imdb_result(response) | ||||||
|  |  | ||||||
|  | def _assemble_imdb_result(votes, rating, top250): | ||||||
|  |     result = {} | ||||||
|  |     if top250: | ||||||
|  |         result['info'] = {'top250': top250} | ||||||
|  |     if votes and rating: | ||||||
|  |         result['ratings'] = {'imdb': {'votes': votes, 'rating': rating}} | ||||||
|  |     return result | ||||||
|  |  | ||||||
|  | def _parse_imdb_result(input_html): | ||||||
|  |     rating = _parse_imdb_rating(input_html) | ||||||
|  |     votes = _parse_imdb_votes(input_html) | ||||||
|  |     top250 = _parse_imdb_top250(input_html) | ||||||
|  |  | ||||||
|  |     return votes, rating, top250 | ||||||
|  |  | ||||||
|  | def _parse_imdb_rating(input_html): | ||||||
|  |     match = re.search(IMDB_RATING_REGEX, input_html) | ||||||
|  |     if (match): | ||||||
|  |         return float(match.group(1)) | ||||||
|  |     return None | ||||||
|  |  | ||||||
|  | def _parse_imdb_votes(input_html): | ||||||
|  |     match = re.search(IMDB_VOTES_REGEX, input_html) | ||||||
|  |     if (match): | ||||||
|  |         return int(match.group(1).replace(',', '')) | ||||||
|  |     return None | ||||||
|  |  | ||||||
|  | def _parse_imdb_top250(input_html): | ||||||
|  |     match = re.search(IMDB_TOP250_REGEX, input_html) | ||||||
|  |     if (match): | ||||||
|  |         return int(match.group(1)) | ||||||
|  |     return None | ||||||
							
								
								
									
										249
									
								
								plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/tmdb.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										249
									
								
								plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/tmdb.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,249 @@ | |||||||
|  | from datetime import datetime, timedelta | ||||||
|  | from . import tmdbapi | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TMDBMovieScraper(object): | ||||||
|  |     def __init__(self, url_settings, language, certification_country): | ||||||
|  |         self.url_settings          = url_settings | ||||||
|  |         self.language              = language | ||||||
|  |         self.certification_country = certification_country | ||||||
|  |         self._urls                 = None | ||||||
|  |         self.tmdbapi               = tmdbapi | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def urls(self): | ||||||
|  |         if not self._urls: | ||||||
|  |             self._urls = _load_base_urls(self.url_settings) | ||||||
|  |         return self._urls | ||||||
|  |  | ||||||
|  |     def search(self, title, year=None): | ||||||
|  |         search_media_id = _parse_media_id(title) | ||||||
|  |         if search_media_id: | ||||||
|  |             if search_media_id['type'] == 'tmdb': | ||||||
|  |                 result = _get_movie(search_media_id['id'], self.language, True) | ||||||
|  |                 result = [result] | ||||||
|  |             else: | ||||||
|  |                 response = tmdbapi.find_movie_by_external_id(search_media_id['id'], language=self.language) | ||||||
|  |                 theerror = response.get('error') | ||||||
|  |                 if theerror: | ||||||
|  |                     return 'error: {}'.format(theerror) | ||||||
|  |                 result = response.get('movie_results') | ||||||
|  |             if 'error' in result: | ||||||
|  |                 return result | ||||||
|  |         else: | ||||||
|  |             response = tmdbapi.search_movie(query=title, year=year, language=self.language) | ||||||
|  |             theerror = response.get('error') | ||||||
|  |             if theerror: | ||||||
|  |                 return 'error: {}'.format(theerror) | ||||||
|  |             result = response['results'] | ||||||
|  |         urls = self.urls | ||||||
|  |  | ||||||
|  |         def is_best(item): | ||||||
|  |             return item['title'].lower() == title and ( | ||||||
|  |                 not year or item.get('release_date', '').startswith(year)) | ||||||
|  |         if result and not is_best(result[0]): | ||||||
|  |             best_first = next((item for item in result if is_best(item)), None) | ||||||
|  |             if best_first: | ||||||
|  |                 result = [best_first] + [item for item in result if item is not best_first] | ||||||
|  |  | ||||||
|  |         for item in result: | ||||||
|  |             if item.get('poster_path'): | ||||||
|  |                 item['poster_path'] = urls['preview'] + item['poster_path'] | ||||||
|  |             if item.get('backdrop_path'): | ||||||
|  |                 item['backdrop_path'] = urls['preview'] + item['backdrop_path'] | ||||||
|  |         return result | ||||||
|  |  | ||||||
|  |     def get_details(self, uniqueids): | ||||||
|  |         media_id = uniqueids.get('tmdb') or uniqueids.get('imdb') | ||||||
|  |         details = self._gather_details(media_id) | ||||||
|  |         if not details: | ||||||
|  |             return None | ||||||
|  |         if details.get('error'): | ||||||
|  |             return details | ||||||
|  |         return self._assemble_details(**details) | ||||||
|  |  | ||||||
|  |     def _gather_details(self, media_id): | ||||||
|  |         movie = _get_movie(media_id, self.language) | ||||||
|  |         if not movie or movie.get('error'): | ||||||
|  |             return movie | ||||||
|  |  | ||||||
|  |         # Don't specify language to get English text for fallback | ||||||
|  |         movie_fallback = _get_movie(media_id) | ||||||
|  |  | ||||||
|  |         collection = _get_moviecollection(movie['belongs_to_collection'].get('id'), self.language) if \ | ||||||
|  |             movie['belongs_to_collection'] else None | ||||||
|  |         collection_fallback = _get_moviecollection(movie['belongs_to_collection'].get('id')) if \ | ||||||
|  |             movie['belongs_to_collection'] else None | ||||||
|  |  | ||||||
|  |         return {'movie': movie, 'movie_fallback': movie_fallback, 'collection': collection, | ||||||
|  |             'collection_fallback': collection_fallback} | ||||||
|  |  | ||||||
|  |     def _assemble_details(self, movie, movie_fallback, collection, collection_fallback): | ||||||
|  |         info = { | ||||||
|  |             'title': movie['title'], | ||||||
|  |             'originaltitle': movie['original_title'], | ||||||
|  |             'plot': movie.get('overview') or movie_fallback.get('overview'), | ||||||
|  |             'tagline': movie.get('tagline') or movie_fallback.get('tagline'), | ||||||
|  |             'studio': _get_names(movie['production_companies']), | ||||||
|  |             'genre': _get_names(movie['genres']), | ||||||
|  |             'country': _get_names(movie['production_countries']), | ||||||
|  |             'credits': _get_cast_members(movie['casts'], 'crew', 'Writing', ['Screenplay', 'Writer', 'Author']), | ||||||
|  |             'director': _get_cast_members(movie['casts'], 'crew', 'Directing', ['Director']), | ||||||
|  |             'premiered': movie['release_date'], | ||||||
|  |             'tag': _get_names(movie['keywords']['keywords']) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if 'countries' in movie['releases']: | ||||||
|  |             certcountry = self.certification_country.upper() | ||||||
|  |             for country in movie['releases']['countries']: | ||||||
|  |                 if country['iso_3166_1'] == certcountry and country['certification']: | ||||||
|  |                     info['mpaa'] = country['certification'] | ||||||
|  |                     break | ||||||
|  |  | ||||||
|  |         trailer = _parse_trailer(movie.get('trailers', {}), movie_fallback.get('trailers', {})) | ||||||
|  |         if trailer: | ||||||
|  |             info['trailer'] = trailer | ||||||
|  |         if collection: | ||||||
|  |             info['set'] = collection.get('name') or collection_fallback.get('name') | ||||||
|  |             info['setoverview'] = collection.get('overview') or collection_fallback.get('overview') | ||||||
|  |         if movie.get('runtime'): | ||||||
|  |             info['duration'] = movie['runtime'] * 60 | ||||||
|  |  | ||||||
|  |         ratings = {'themoviedb': {'rating': float(movie['vote_average']), 'votes': int(movie['vote_count'])}} | ||||||
|  |         uniqueids = {'tmdb': movie['id'], 'imdb': movie['imdb_id']} | ||||||
|  |         cast = [{ | ||||||
|  |                 'name': actor['name'], | ||||||
|  |                 'role': actor['character'], | ||||||
|  |                 'thumbnail': self.urls['original'] + actor['profile_path'] | ||||||
|  |                     if actor['profile_path'] else "", | ||||||
|  |                 'order': actor['order'] | ||||||
|  |             } | ||||||
|  |             for actor in movie['casts'].get('cast', []) | ||||||
|  |         ] | ||||||
|  |         available_art = _parse_artwork(movie, collection, self.urls, self.language) | ||||||
|  |  | ||||||
|  |         _info = {'set_tmdbid': movie['belongs_to_collection'].get('id') | ||||||
|  |             if movie['belongs_to_collection'] else None} | ||||||
|  |  | ||||||
|  |         return {'info': info, 'ratings': ratings, 'uniqueids': uniqueids, 'cast': cast, | ||||||
|  |             'available_art': available_art, '_info': _info} | ||||||
|  |  | ||||||
|  | def _parse_media_id(title): | ||||||
|  |     if title.startswith('tt') and title[2:].isdigit(): | ||||||
|  |         return {'type': 'imdb', 'id':title} # IMDB ID works alone because it is clear | ||||||
|  |     title = title.lower() | ||||||
|  |     if title.startswith('tmdb/') and title[5:].isdigit(): # TMDB ID | ||||||
|  |         return {'type': 'tmdb', 'id':title[5:]} | ||||||
|  |     elif title.startswith('imdb/tt') and title[7:].isdigit(): # IMDB ID with prefix to match | ||||||
|  |         return {'type': 'imdb', 'id':title[5:]} | ||||||
|  |     return None | ||||||
|  |  | ||||||
|  | def _get_movie(mid, language=None, search=False): | ||||||
|  |     details = None if search else \ | ||||||
|  |         'trailers,images,releases,casts,keywords' if language is not None else \ | ||||||
|  |         'trailers' | ||||||
|  |     response = tmdbapi.get_movie(mid, language=language, append_to_response=details) | ||||||
|  |     theerror = response.get('error') | ||||||
|  |     if theerror: | ||||||
|  |         return 'error: {}'.format(theerror) | ||||||
|  |     else: | ||||||
|  |         return response | ||||||
|  |  | ||||||
|  | def _get_moviecollection(collection_id, language=None): | ||||||
|  |     if not collection_id: | ||||||
|  |         return None | ||||||
|  |     details = 'images' | ||||||
|  |     response = tmdbapi.get_collection(collection_id, language=language, append_to_response=details) | ||||||
|  |     theerror = response.get('error') | ||||||
|  |     if theerror: | ||||||
|  |         return 'error: {}'.format(theerror) | ||||||
|  |     else: | ||||||
|  |         return response | ||||||
|  |  | ||||||
|  | def _parse_artwork(movie, collection, urlbases, language): | ||||||
|  |     if language: | ||||||
|  |         # Image languages don't have regional variants | ||||||
|  |         language = language.split('-')[0] | ||||||
|  |     posters = [] | ||||||
|  |     landscape = [] | ||||||
|  |     fanart = [] | ||||||
|  |     if 'images' in movie: | ||||||
|  |         posters = _get_images_with_fallback(movie['images']['posters'], urlbases, language) | ||||||
|  |         landscape = _get_images(movie['images']['backdrops'], urlbases, language) | ||||||
|  |         fanart = _get_images(movie['images']['backdrops'], urlbases, None) | ||||||
|  |  | ||||||
|  |     setposters = [] | ||||||
|  |     setlandscape = [] | ||||||
|  |     setfanart = [] | ||||||
|  |     if collection and 'images' in collection: | ||||||
|  |         setposters = _get_images_with_fallback(collection['images']['posters'], urlbases, language) | ||||||
|  |         setlandscape = _get_images(collection['images']['backdrops'], urlbases, language) | ||||||
|  |         setfanart = _get_images(collection['images']['backdrops'], urlbases, None) | ||||||
|  |  | ||||||
|  |     return {'poster': posters, 'landscape': landscape, 'fanart': fanart, | ||||||
|  |         'set.poster': setposters, 'set.landscape': setlandscape, 'set.fanart': setfanart} | ||||||
|  |  | ||||||
|  | def _get_images_with_fallback(imagelist, urlbases, language, language_fallback='en'): | ||||||
|  |     images = _get_images(imagelist, urlbases, language) | ||||||
|  |  | ||||||
|  |     # Add backup images | ||||||
|  |     if language != language_fallback: | ||||||
|  |         images.extend(_get_images(imagelist, urlbases, language_fallback)) | ||||||
|  |  | ||||||
|  |     # Add any images if nothing set so far | ||||||
|  |     if not images: | ||||||
|  |         images = _get_images(imagelist, urlbases) | ||||||
|  |  | ||||||
|  |     return images | ||||||
|  |  | ||||||
|  | def _get_images(imagelist, urlbases, language='_any'): | ||||||
|  |     result = [] | ||||||
|  |     for img in imagelist: | ||||||
|  |         if language != '_any' and img['iso_639_1'] != language: | ||||||
|  |             continue | ||||||
|  |         result.append({ | ||||||
|  |             'url': urlbases['original'] + img['file_path'], | ||||||
|  |             'preview': urlbases['preview'] + img['file_path'], | ||||||
|  |         }) | ||||||
|  |     return result | ||||||
|  |  | ||||||
|  | def _get_date_numeric(datetime_): | ||||||
|  |     return (datetime_ - datetime(1970, 1, 1)).total_seconds() | ||||||
|  |  | ||||||
|  | def _load_base_urls(url_settings): | ||||||
|  |     urls = {} | ||||||
|  |     # urls['original'] = url_settings.getSettingString('originalUrl') | ||||||
|  |     # urls['preview']  = url_settings.getSettingString('previewUrl') | ||||||
|  |     # last_updated     = url_settings.getSettingString('lastUpdated') | ||||||
|  |     urls['original'] = "" | ||||||
|  |     urls['preview']  = "" | ||||||
|  |     last_updated     = "0" | ||||||
|  |  | ||||||
|  |     if not urls['original'] or not urls['preview'] or not last_updated or \ | ||||||
|  |             float(last_updated) < _get_date_numeric(datetime.now() - timedelta(days=30)): | ||||||
|  |         conf = tmdbapi.get_configuration() | ||||||
|  |         if conf: | ||||||
|  |             urls['original'] = conf['images']['secure_base_url'] + 'original' | ||||||
|  |             urls['preview'] = conf['images']['secure_base_url'] + 'w780' | ||||||
|  |             # url_settings.setSetting('originalUrl', urls['original']) | ||||||
|  |             # url_settings.setSetting('previewUrl', urls['preview']) | ||||||
|  |             # url_settings.setSetting('lastUpdated', str(_get_date_numeric(datetime.now()))) | ||||||
|  |     return urls | ||||||
|  |  | ||||||
|  | def _parse_trailer(trailers, fallback): | ||||||
|  |     if trailers.get('youtube'): | ||||||
|  |         return 'plugin://plugin.video.youtube/?action=play_video&videoid='+trailers['youtube'][0]['source'] | ||||||
|  |     if fallback.get('youtube'): | ||||||
|  |         return 'plugin://plugin.video.youtube/?action=play_video&videoid='+fallback['youtube'][0]['source'] | ||||||
|  |     return None | ||||||
|  |  | ||||||
|  | def _get_names(items): | ||||||
|  |     return [item['name'] for item in items] if items else [] | ||||||
|  |  | ||||||
|  | def _get_cast_members(casts, casttype, department, jobs): | ||||||
|  |     result = [] | ||||||
|  |     if casttype in casts: | ||||||
|  |         for cast in casts[casttype]: | ||||||
|  |             if cast['department'] == department and cast['job'] in jobs and cast['name'] not in result: | ||||||
|  |                 result.append(cast['name']) | ||||||
|  |     return result | ||||||
							
								
								
									
										129
									
								
								plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/tmdbapi.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/tmdbapi.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | |||||||
|  | # -*- coding: UTF-8 -*- | ||||||
|  | # | ||||||
|  | # Copyright (C) 2020, Team Kodi | ||||||
|  | # | ||||||
|  | # This program is free software: you can redistribute it and/or modify | ||||||
|  | # it under the terms of the GNU General Public License as published by | ||||||
|  | # the Free Software Foundation, either version 3 of the License, or | ||||||
|  | # (at your option) any later version. | ||||||
|  | # | ||||||
|  | # This program is distributed in the hope that it will be useful, | ||||||
|  | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | # GNU General Public License for more details. | ||||||
|  | # | ||||||
|  | # You should have received a copy of the GNU General Public License | ||||||
|  | # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
|  | # pylint: disable=missing-docstring | ||||||
|  |  | ||||||
|  | """Functions to interact with TMDb API.""" | ||||||
|  |  | ||||||
|  | from . import api_utils | ||||||
|  | try: | ||||||
|  |     from typing import Optional, Text, Dict, List, Any  # pylint: disable=unused-import | ||||||
|  |     InfoType = Dict[Text, Any]  # pylint: disable=invalid-name | ||||||
|  | except ImportError: | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | HEADERS = ( | ||||||
|  |     ('User-Agent', 'Kodi Movie scraper by Team Kodi'), | ||||||
|  |     ('Accept', 'application/json'), | ||||||
|  | ) | ||||||
|  | api_utils.set_headers(dict(HEADERS)) | ||||||
|  |  | ||||||
|  | TMDB_PARAMS = {'api_key': 'f090bb54758cabf231fb605d3e3e0468'} | ||||||
|  | BASE_URL = 'https://api.themoviedb.org/3/{}' | ||||||
|  | SEARCH_URL = BASE_URL.format('search/movie') | ||||||
|  | FIND_URL = BASE_URL.format('find/{}') | ||||||
|  | MOVIE_URL = BASE_URL.format('movie/{}') | ||||||
|  | COLLECTION_URL = BASE_URL.format('collection/{}') | ||||||
|  | CONFIG_URL = BASE_URL.format('configuration') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def search_movie(query, year=None, language=None): | ||||||
|  |     # type: (Text) -> List[InfoType] | ||||||
|  |     """ | ||||||
|  |     Search for a movie | ||||||
|  |  | ||||||
|  |     :param title: movie title to search | ||||||
|  |     :param year: the year to search (optional) | ||||||
|  |     :param language: the language filter for TMDb (optional) | ||||||
|  |     :return: a list with found movies | ||||||
|  |     """ | ||||||
|  |     theurl = SEARCH_URL | ||||||
|  |     params = _set_params(None, language) | ||||||
|  |     params['query'] = query | ||||||
|  |     if year is not None: | ||||||
|  |         params['year'] = str(year) | ||||||
|  |     return api_utils.load_info(theurl, params=params) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def find_movie_by_external_id(external_id, language=None): | ||||||
|  |     # type: (Text) -> List[InfoType] | ||||||
|  |     """ | ||||||
|  |     Find movie based on external ID | ||||||
|  |  | ||||||
|  |     :param mid: external ID | ||||||
|  |     :param language: the language filter for TMDb (optional) | ||||||
|  |     :return: the movie or error | ||||||
|  |     """ | ||||||
|  |     theurl = FIND_URL.format(external_id) | ||||||
|  |     params = _set_params(None, language) | ||||||
|  |     params['external_source'] = 'imdb_id' | ||||||
|  |     return api_utils.load_info(theurl, params=params) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_movie(mid, language=None, append_to_response=None): | ||||||
|  |     # type: (Text) -> List[InfoType] | ||||||
|  |     """ | ||||||
|  |     Get movie details | ||||||
|  |  | ||||||
|  |     :param mid: TMDb movie ID | ||||||
|  |     :param language: the language filter for TMDb (optional) | ||||||
|  |     :append_to_response: the additional data to get from TMDb (optional) | ||||||
|  |     :return: the movie or error | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         theurl = MOVIE_URL.format(mid) | ||||||
|  |         return api_utils.load_info(theurl, params=_set_params(append_to_response, language)) | ||||||
|  |     except Exception as e: | ||||||
|  |         print(repr(e))  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_collection(collection_id, language=None, append_to_response=None): | ||||||
|  |     # type: (Text) -> List[InfoType] | ||||||
|  |     """ | ||||||
|  |     Get movie collection information | ||||||
|  |  | ||||||
|  |     :param collection_id: TMDb collection ID | ||||||
|  |     :param language: the language filter for TMDb (optional) | ||||||
|  |     :append_to_response: the additional data to get from TMDb (optional) | ||||||
|  |     :return: the movie or error | ||||||
|  |     """ | ||||||
|  |     theurl = COLLECTION_URL.format(collection_id) | ||||||
|  |     return api_utils.load_info(theurl, params=_set_params(append_to_response, language)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_configuration(): | ||||||
|  |     # type: (Text) -> List[InfoType] | ||||||
|  |     """ | ||||||
|  |     Get configuration information | ||||||
|  |  | ||||||
|  |     :return: configuration details or error | ||||||
|  |     """ | ||||||
|  |     return api_utils.load_info(CONFIG_URL, params=TMDB_PARAMS.copy()) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _set_params(append_to_response, language): | ||||||
|  |     params = TMDB_PARAMS.copy() | ||||||
|  |     img_lang = 'en,null' | ||||||
|  |     if language is not None: | ||||||
|  |         params['language'] = language | ||||||
|  |         img_lang = '%s,en,null' % language[0:2] | ||||||
|  |     if append_to_response is not None: | ||||||
|  |         params['append_to_response'] = append_to_response | ||||||
|  |         if 'images' in append_to_response: | ||||||
|  |             params['include_image_language'] = img_lang | ||||||
|  |     return params | ||||||
| @@ -0,0 +1,55 @@ | |||||||
|  | # -*- coding: UTF-8 -*- | ||||||
|  | # | ||||||
|  | # Copyright (C) 2020, Team Kodi | ||||||
|  | # | ||||||
|  | # This program is free software: you can redistribute it and/or modify | ||||||
|  | # it under the terms of the GNU General Public License as published by | ||||||
|  | # the Free Software Foundation, either version 3 of the License, or | ||||||
|  | # (at your option) any later version. | ||||||
|  | # | ||||||
|  | # This program is distributed in the hope that it will be useful, | ||||||
|  | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | # GNU General Public License for more details. | ||||||
|  | # | ||||||
|  | # You should have received a copy of the GNU General Public License | ||||||
|  | # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
|  | # pylint: disable=missing-docstring | ||||||
|  |  | ||||||
|  | """Functions to interact with Trakt API.""" | ||||||
|  |  | ||||||
|  | from __future__ import absolute_import, unicode_literals | ||||||
|  |  | ||||||
|  | from . import api_utils | ||||||
|  | from . import get_imdb_id | ||||||
|  | try: | ||||||
|  |     from typing import Optional, Text, Dict, List, Any  # pylint: disable=unused-import | ||||||
|  |     InfoType = Dict[Text, Any]  # pylint: disable=invalid-name | ||||||
|  | except ImportError: | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | HEADERS = ( | ||||||
|  |     ('User-Agent', 'Kodi Movie scraper by Team Kodi'), | ||||||
|  |     ('Accept', 'application/json'), | ||||||
|  |     ('trakt-api-key', '5f2dc73b6b11c2ac212f5d8b4ec8f3dc4b727bb3f026cd254d89eda997fe64ae'), | ||||||
|  |     ('trakt-api-version', '2'), | ||||||
|  |     ('Content-Type', 'application/json'), | ||||||
|  | ) | ||||||
|  | api_utils.set_headers(dict(HEADERS)) | ||||||
|  |  | ||||||
|  | MOVIE_URL = 'https://api.trakt.tv/movies/{}' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_trakt_ratinginfo(uniqueids): | ||||||
|  |     imdb_id = get_imdb_id(uniqueids) | ||||||
|  |     result = {} | ||||||
|  |     url = MOVIE_URL.format(imdb_id) | ||||||
|  |     params = {'extended': 'full'} | ||||||
|  |     movie_info = api_utils.load_info(url, params=params, default={}) | ||||||
|  |     if(movie_info): | ||||||
|  |         if 'votes' in movie_info and 'rating' in movie_info: | ||||||
|  |             result['ratings'] = {'trakt': {'votes': int(movie_info['votes']), 'rating': float(movie_info['rating'])}} | ||||||
|  |         elif 'rating' in movie_info: | ||||||
|  |             result['ratings'] = {'trakt': {'rating': float(movie_info['rating'])}} | ||||||
|  |     return result | ||||||
							
								
								
									
										
											BIN
										
									
								
								plugins/translate/brotli/_brotli.abi3.so
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								plugins/translate/brotli/_brotli.abi3.so
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -77,8 +77,7 @@ class Controller(UIMixin, SignalsMixins, Controller_Data): | |||||||
|         event_system.subscribe("set_clipboard_data", self.set_clipboard_data) |         event_system.subscribe("set_clipboard_data", self.set_clipboard_data) | ||||||
|  |  | ||||||
|     def _load_glade_file(self): |     def _load_glade_file(self): | ||||||
|         self.builder = Gtk.Builder() |         self.builder.add_from_file( settings_manager.get_glade_file() ) | ||||||
|         self.builder.add_from_file(settings_manager.get_glade_file()) |  | ||||||
|         self.builder.expose_object("main_window", self.window) |         self.builder.expose_object("main_window", self.window) | ||||||
|  |  | ||||||
|         self.core_widget = self.builder.get_object("core_widget") |         self.core_widget = self.builder.get_object("core_widget") | ||||||
|   | |||||||
| @@ -35,13 +35,39 @@ class State: | |||||||
|     user_pass_dialog: type  = None |     user_pass_dialog: type  = None | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SFMBuilder(Gtk.Builder): | ||||||
|  |     """docstring for SFMBuilder.""" | ||||||
|  |  | ||||||
|  |     def __init__(self): | ||||||
|  |         super(SFMBuilder, self).__init__() | ||||||
|  |  | ||||||
|  |         self.objects = {} | ||||||
|  |  | ||||||
|  |     def get_object(self, id: str, use_gtk: bool = True) -> any: | ||||||
|  |         if not use_gtk: | ||||||
|  |             return self.objects[id] | ||||||
|  |  | ||||||
|  |         return super(SFMBuilder, self).get_object(id) | ||||||
|  |  | ||||||
|  |     def expose_object(self, id: str, object: any, use_gtk: bool = True) -> None: | ||||||
|  |         if not use_gtk: | ||||||
|  |             self.objects[id] = object | ||||||
|  |         else: | ||||||
|  |             super(SFMBuilder, self).expose_object(id, object) | ||||||
|  |  | ||||||
|  |     def dereference_object(self, id: str) -> None: | ||||||
|  |         del self.objects[id] | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Controller_Data: | class Controller_Data: | ||||||
|     """ Controller_Data contains most of the state of the app at ay given time. It also has some support methods. """ |     """ Controller_Data contains most of the state of the app at ay given time. It also has some support methods. """ | ||||||
|     __slots__ = "settings", "builder", "logger", "keybindings", "trashman", "fm_controller", "window", "window1", "window2", "window3", "window4" |     __slots__ = "settings", "builder", "logger", "keybindings", "trashman", "fm_controller", "window", "window1", "window2", "window3", "window4" | ||||||
|  |  | ||||||
|     def _setup_controller_data(self) -> None: |     def _setup_controller_data(self) -> None: | ||||||
|         self.window        = settings_manager.get_main_window() |         self.window        = settings_manager.get_main_window() | ||||||
|         self.builder       = None |         self.builder       = SFMBuilder() | ||||||
|         self.core_widget   = None |         self.core_widget   = None | ||||||
|  |  | ||||||
|         self._load_glade_file() |         self._load_glade_file() | ||||||
| @@ -88,7 +114,7 @@ class Controller_Data: | |||||||
|         state.notebooks        = self.notebooks |         state.notebooks        = self.notebooks | ||||||
|         state.wid, state.tid   = self.fm_controller.get_active_wid_and_tid() |         state.wid, state.tid   = self.fm_controller.get_active_wid_and_tid() | ||||||
|         state.tab              = self.get_fm_window(state.wid).get_tab_by_id(state.tid) |         state.tab              = self.get_fm_window(state.wid).get_tab_by_id(state.tid) | ||||||
|         state.icon_grid        = self.builder.get_object(f"{state.wid}|{state.tid}|icon_grid") |         state.icon_grid        = self.builder.get_object(f"{state.wid}|{state.tid}|icon_grid", use_gtk = False) | ||||||
|         # state.icon_grid        = event_system.emit_and_await("get_files_view_icon_grid", (state.wid, state.tid)) |         # state.icon_grid        = event_system.emit_and_await("get_files_view_icon_grid", (state.wid, state.tid)) | ||||||
|         state.store            = state.icon_grid.get_model() |         state.store            = state.icon_grid.get_model() | ||||||
|         state.message_dialog   = MessageWidget() |         state.message_dialog   = MessageWidget() | ||||||
| @@ -110,7 +136,7 @@ class Controller_Data: | |||||||
|         event_system.emit("update_state_info_plugins", state) # NOTE: Need to remove after we convert plugins to use emit_and_await |         event_system.emit("update_state_info_plugins", state) # NOTE: Need to remove after we convert plugins to use emit_and_await | ||||||
|         return state |         return state | ||||||
|  |  | ||||||
|     def format_to_uris(self, store, wid, tid, treePaths, use_just_path=False): |     def format_to_uris(self, store, wid, tid, treePaths, use_just_path = False): | ||||||
|         tab  = self.get_fm_window(wid).get_tab_by_id(tid) |         tab  = self.get_fm_window(wid).get_tab_by_id(tid) | ||||||
|         dir  = tab.get_current_directory() |         dir  = tab.get_current_directory() | ||||||
|         uris = [] |         uris = [] | ||||||
|   | |||||||
| @@ -35,7 +35,7 @@ class FileActionSignalsMixin: | |||||||
|  |  | ||||||
|     # NOTE: Too lazy to impliment a proper update handler and so just regen store and update tab. |     # NOTE: Too lazy to impliment a proper update handler and so just regen store and update tab. | ||||||
|     #       Use a lock system to prevent too many update calls for certain instances but user can manually refresh if they have urgency |     #       Use a lock system to prevent too many update calls for certain instances but user can manually refresh if they have urgency | ||||||
|     def dir_watch_updates(self, file_monitor, file, other_file=None, eve_type=None, data=None): |     def dir_watch_updates(self, file_monitor, file, other_file = None, eve_type = None, data = None): | ||||||
|         if eve_type in  [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.DELETED, |         if eve_type in  [Gio.FileMonitorEvent.CREATED, Gio.FileMonitorEvent.DELETED, | ||||||
|                         Gio.FileMonitorEvent.RENAMED, Gio.FileMonitorEvent.MOVED_IN, |                         Gio.FileMonitorEvent.RENAMED, Gio.FileMonitorEvent.MOVED_IN, | ||||||
|                         Gio.FileMonitorEvent.MOVED_OUT]: |                         Gio.FileMonitorEvent.MOVED_OUT]: | ||||||
| @@ -68,7 +68,7 @@ class FileActionSignalsMixin: | |||||||
|         wid, tid  = tab_widget.split("|") |         wid, tid  = tab_widget.split("|") | ||||||
|         notebook  = self.builder.get_object(f"window_{wid}") |         notebook  = self.builder.get_object(f"window_{wid}") | ||||||
|         tab       = self.get_fm_window(wid).get_tab_by_id(tid) |         tab       = self.get_fm_window(wid).get_tab_by_id(tid) | ||||||
|         icon_grid = self.builder.get_object(f"{wid}|{tid}|icon_grid") |         icon_grid = self.builder.get_object(f"{wid}|{tid}|icon_grid", use_gtk = False) | ||||||
|         store     = icon_grid.get_model() |         store     = icon_grid.get_model() | ||||||
|         _store, tab_widget_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}") |         _store, tab_widget_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}") | ||||||
|  |  | ||||||
| @@ -82,7 +82,7 @@ class FileActionSignalsMixin: | |||||||
|             self.set_bottom_labels(tab) |             self.set_bottom_labels(tab) | ||||||
|  |  | ||||||
|  |  | ||||||
|     def do_file_search(self, widget, eve=None): |     def do_file_search(self, widget, eve = None): | ||||||
|         if not self.ctrl_down and not self.shift_down and not self.alt_down: |         if not self.ctrl_down and not self.shift_down and not self.alt_down: | ||||||
|             target    = widget.get_name() |             target    = widget.get_name() | ||||||
|             notebook  = self.builder.get_object(target) |             notebook  = self.builder.get_object(target) | ||||||
|   | |||||||
| @@ -74,7 +74,7 @@ class KeyboardSignalsMixin: | |||||||
|     def keyboard_close_tab(self): |     def keyboard_close_tab(self): | ||||||
|         wid, tid  = self.fm_controller.get_active_wid_and_tid() |         wid, tid  = self.fm_controller.get_active_wid_and_tid() | ||||||
|         notebook  = self.builder.get_object(f"window_{wid}") |         notebook  = self.builder.get_object(f"window_{wid}") | ||||||
|         scroll    = self.builder.get_object(f"{wid}|{tid}") |         scroll    = self.builder.get_object(f"{wid}|{tid}", use_gtk = False) | ||||||
|         page      = notebook.page_num(scroll) |         page      = notebook.page_num(scroll) | ||||||
|         tab       = self.get_fm_window(wid).get_tab_by_id(tid) |         tab       = self.get_fm_window(wid).get_tab_by_id(tid) | ||||||
|         watcher   = tab.get_dir_watcher() |         watcher   = tab.get_dir_watcher() | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| # Python imports | # Python imports | ||||||
|  | import asyncio | ||||||
|  |  | ||||||
| # Lib imports | # Lib imports | ||||||
| import gi | import gi | ||||||
| @@ -31,35 +32,56 @@ class GridMixin: | |||||||
|             for i, icon in enumerate( self.create_icons_generator(tab, dir, files) ): |             for i, icon in enumerate( self.create_icons_generator(tab, dir, files) ): | ||||||
|                 self.load_icon(i, store, icon) |                 self.load_icon(i, store, icon) | ||||||
|         else: |         else: | ||||||
|             for i, file in enumerate(files): |             # for i, file in enumerate(files): | ||||||
|                 self.create_icon(i, tab, store, dir, file[0]) |             #     self.create_icon(i, tab, store, dir, file[0]) | ||||||
|  |             try: | ||||||
|  |                 loop = asyncio.get_running_loop() | ||||||
|  |             except RuntimeError: | ||||||
|  |                 loop = None | ||||||
|  |  | ||||||
|  |             if loop and loop.is_running(): | ||||||
|  |                 loop.create_task( self.create_icons(tab, store, dir, files) ) | ||||||
|  |             else: | ||||||
|  |                 asyncio.run( self.create_icons(tab, store, dir, files) ) | ||||||
|  |  | ||||||
|         # NOTE: Not likely called often from here but it could be useful |         # NOTE: Not likely called often from here but it could be useful | ||||||
|         if save_state and not trace_debug: |         if save_state and not trace_debug: | ||||||
|             self.fm_controller.save_state() |             self.fm_controller.save_state() | ||||||
|  |  | ||||||
|  |     async def create_icons(self, tab, store, dir, files): | ||||||
|  |         tasks = [self.update_store(i, store, dir, tab, file[0]) for i, file in enumerate(files)] | ||||||
|  |         await asyncio.gather(*tasks) | ||||||
|  |  | ||||||
|  |     async def load_icon(self, i, store, icon): | ||||||
|  |         self.update_store(i, store, icon) | ||||||
|  |  | ||||||
|  |     async def update_store(self, i, store, dir, tab, file): | ||||||
|  |         icon = tab.create_icon(dir, file) | ||||||
|  |         itr  = store.get_iter(i) | ||||||
|  |         store.set_value(itr, 0, icon) | ||||||
|  |  | ||||||
|     def create_icons_generator(self, tab, dir, files): |     def create_icons_generator(self, tab, dir, files): | ||||||
|         for file in files: |         for file in files: | ||||||
|             icon = tab.create_icon(dir, file[0]) |             icon = tab.create_icon(dir, file[0]) | ||||||
|             yield icon |             yield icon | ||||||
|  |  | ||||||
|     @daemon_threaded |     # @daemon_threaded | ||||||
|     def create_icon(self, i, tab, store, dir, file): |     # def create_icon(self, i, tab, store, dir, file): | ||||||
|         icon = tab.create_icon(dir, file) |     #     icon = tab.create_icon(dir, file) | ||||||
|         GLib.idle_add(self.update_store, *(i, store, icon,)) |     #     GLib.idle_add(self.update_store, *(i, store, icon,)) | ||||||
|  |     # | ||||||
|  |     # @daemon_threaded | ||||||
|  |     # def load_icon(self, i, store, icon): | ||||||
|  |     #     GLib.idle_add(self.update_store, *(i, store, icon,)) | ||||||
|  |  | ||||||
|     @daemon_threaded |     # def update_store(self, i, store, icon): | ||||||
|     def load_icon(self, i, store, icon): |     #     itr = store.get_iter(i) | ||||||
|         GLib.idle_add(self.update_store, *(i, store, icon,)) |     #     store.set_value(itr, 0, icon) | ||||||
|  |  | ||||||
|     def update_store(self, i, store, icon): |  | ||||||
|         itr = store.get_iter(i) |  | ||||||
|         store.set_value(itr, 0, icon) |  | ||||||
|  |  | ||||||
|     def create_tab_widget(self, tab): |     def create_tab_widget(self, tab): | ||||||
|         return TabHeaderWidget(tab, self.close_tab) |         return TabHeaderWidget(tab, self.close_tab) | ||||||
|  |  | ||||||
|     def create_scroll_and_store(self, tab, wid, use_tree_view=False): |     def create_scroll_and_store(self, tab, wid, use_tree_view = False): | ||||||
|         scroll = Gtk.ScrolledWindow() |         scroll = Gtk.ScrolledWindow() | ||||||
|  |  | ||||||
|         if not use_tree_view: |         if not use_tree_view: | ||||||
| @@ -71,8 +93,8 @@ class GridMixin: | |||||||
|         scroll.add(grid) |         scroll.add(grid) | ||||||
|         scroll.set_name(f"{wid}|{tab.get_id()}") |         scroll.set_name(f"{wid}|{tab.get_id()}") | ||||||
|         grid.set_name(f"{wid}|{tab.get_id()}") |         grid.set_name(f"{wid}|{tab.get_id()}") | ||||||
|         self.builder.expose_object(f"{wid}|{tab.get_id()}|icon_grid", grid) |         self.builder.expose_object(f"{wid}|{tab.get_id()}|icon_grid", grid, use_gtk = False) | ||||||
|         self.builder.expose_object(f"{wid}|{tab.get_id()}", scroll) |         self.builder.expose_object(f"{wid}|{tab.get_id()}", scroll, use_gtk = False) | ||||||
|  |  | ||||||
|         return scroll, grid.get_store() |         return scroll, grid.get_store() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ from .grid_mixin import GridMixin | |||||||
| class TabMixin(GridMixin): | class TabMixin(GridMixin): | ||||||
|     """docstring for TabMixin""" |     """docstring for TabMixin""" | ||||||
|  |  | ||||||
|     def create_tab(self, wid=None, tid=None, path=None): |     def create_tab(self, wid: int = None, tid: int = None, path: str = None): | ||||||
|         if not wid: |         if not wid: | ||||||
|             wid, tid = self.fm_controller.get_active_wid_and_tid() |             wid, tid = self.fm_controller.get_active_wid_and_tid() | ||||||
|  |  | ||||||
| @@ -60,7 +60,7 @@ class TabMixin(GridMixin): | |||||||
|         tab_box   = button.get_parent() |         tab_box   = button.get_parent() | ||||||
|         wid       = int(notebook.get_name()[-1]) |         wid       = int(notebook.get_name()[-1]) | ||||||
|         tid       = self.get_id_from_tab_box(tab_box) |         tid       = self.get_id_from_tab_box(tab_box) | ||||||
|         scroll    = self.builder.get_object(f"{wid}|{tid}") |         scroll    = self.builder.get_object(f"{wid}|{tid}", use_gtk = False) | ||||||
|         icon_grid = scroll.get_children()[0] |         icon_grid = scroll.get_children()[0] | ||||||
|         store     = icon_grid.get_store() |         store     = icon_grid.get_store() | ||||||
|         tab       = self.get_fm_window(wid).get_tab_by_id(tid) |         tab       = self.get_fm_window(wid).get_tab_by_id(tid) | ||||||
| @@ -69,6 +69,9 @@ class TabMixin(GridMixin): | |||||||
|         watcher.cancel() |         watcher.cancel() | ||||||
|         self.get_fm_window(wid).delete_tab_by_id(tid) |         self.get_fm_window(wid).delete_tab_by_id(tid) | ||||||
|  |  | ||||||
|  |         self.builder.dereference_object(f"{wid}|{tid}|icon_grid") | ||||||
|  |         self.builder.dereference_object(f"{wid}|{tid}") | ||||||
|  |  | ||||||
|         store.clear() |         store.clear() | ||||||
|         # store.run_dispose() |         # store.run_dispose() | ||||||
|         icon_grid.destroy() |         icon_grid.destroy() | ||||||
| @@ -112,7 +115,7 @@ class TabMixin(GridMixin): | |||||||
|         if not settings_manager.is_trace_debug(): |         if not settings_manager.is_trace_debug(): | ||||||
|             self.fm_controller.save_state() |             self.fm_controller.save_state() | ||||||
|  |  | ||||||
|     def on_tab_switch_update(self, notebook, content=None, index=None): |     def on_tab_switch_update(self, notebook, content = None, index = None): | ||||||
|         self.selected_files.clear() |         self.selected_files.clear() | ||||||
|         wid, tid = content.get_children()[0].get_name().split("|") |         wid, tid = content.get_children()[0].get_name().split("|") | ||||||
|         self.fm_controller.set_wid_and_tid(wid, tid) |         self.fm_controller.set_wid_and_tid(wid, tid) | ||||||
| @@ -131,7 +134,7 @@ class TabMixin(GridMixin): | |||||||
|     def get_tab_icon_grid_from_notebook(self, notebook): |     def get_tab_icon_grid_from_notebook(self, notebook): | ||||||
|         return notebook.get_children()[1].get_children()[0] |         return notebook.get_children()[1].get_children()[0] | ||||||
|  |  | ||||||
|     def refresh_tab(data=None): |     def refresh_tab(data = None): | ||||||
|         state = self.get_current_state() |         state = self.get_current_state() | ||||||
|         state.tab.load_directory() |         state.tab.load_directory() | ||||||
|         self.load_store(state.tab, state.store) |         self.load_store(state.tab, state.store) | ||||||
| @@ -148,7 +151,7 @@ class TabMixin(GridMixin): | |||||||
|         if not settings_manager.is_trace_debug(): |         if not settings_manager.is_trace_debug(): | ||||||
|             self.fm_controller.save_state() |             self.fm_controller.save_state() | ||||||
|  |  | ||||||
|     def do_action_from_bar_controls(self, widget, eve=None): |     def do_action_from_bar_controls(self, widget, eve = None): | ||||||
|         action    = widget.get_name() |         action    = widget.get_name() | ||||||
|         wid, tid  = self.fm_controller.get_active_wid_and_tid() |         wid, tid  = self.fm_controller.get_active_wid_and_tid() | ||||||
|         notebook  = self.builder.get_object(f"window_{wid}") |         notebook  = self.builder.get_object(f"window_{wid}") | ||||||
|   | |||||||
| @@ -36,6 +36,15 @@ class UIMixin(PaneMixin, WindowMixin): | |||||||
|             isHidden = True if session["window"]["isHidden"] == "True" else False |             isHidden = True if session["window"]["isHidden"] == "True" else False | ||||||
|             event_system.emit("load_files_view_state", (nickname, tabs)) |             event_system.emit("load_files_view_state", (nickname, tabs)) | ||||||
|  |  | ||||||
|  |     @daemon_threaded | ||||||
|  |     def _focus_last_visible_notebook(self, icon_grid): | ||||||
|  |         import time | ||||||
|  |  | ||||||
|  |         window = settings_manager.get_main_window() | ||||||
|  |         while not window.is_visible() and not window.get_realized(): | ||||||
|  |             time.sleep(0.1) | ||||||
|  |  | ||||||
|  |         icon_grid.event(Gdk.Event().new(type = Gdk.EventType.BUTTON_RELEASE)) | ||||||
|  |  | ||||||
|     def _current_loading_process(self, session_json = None): |     def _current_loading_process(self, session_json = None): | ||||||
|         if session_json: |         if session_json: | ||||||
| @@ -58,16 +67,17 @@ class UIMixin(PaneMixin, WindowMixin): | |||||||
|  |  | ||||||
|             try: |             try: | ||||||
|                 if not self.is_pane4_hidden: |                 if not self.is_pane4_hidden: | ||||||
|                     icon_grid = self.window4.get_children()[-1].get_children()[0] |                     notebook = self.window4 | ||||||
|                 elif not self.is_pane3_hidden: |                 elif not self.is_pane3_hidden: | ||||||
|                     icon_grid = self.window3.get_children()[-1].get_children()[0] |                     notebook = self.window3 | ||||||
|                 elif not self.is_pane2_hidden: |                 elif not self.is_pane2_hidden: | ||||||
|                     icon_grid = self.window2.get_children()[-1].get_children()[0] |                     notebook = self.window2 | ||||||
|                 elif not self.is_pane1_hidden: |                 elif not self.is_pane1_hidden: | ||||||
|                     icon_grid = self.window1.get_children()[-1].get_children()[0] |                     notebook = self.window1 | ||||||
|  |  | ||||||
|                 icon_grid.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE)) |                 scroll_win = notebook.get_children()[-1] | ||||||
|                 icon_grid.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE)) |                 icon_grid  = scroll_win.get_children()[0] | ||||||
|  |                 self._focus_last_visible_notebook(icon_grid) | ||||||
|             except UIMixinException as e: |             except UIMixinException as e: | ||||||
|                 logger.info("\n:  The saved session might be missing window data!  :\nLocation: ~/.config/solarfm/session.json\nFix: Back it up and delete it to reset.\n") |                 logger.info("\n:  The saved session might be missing window data!  :\nLocation: ~/.config/solarfm/session.json\nFix: Back it up and delete it to reset.\n") | ||||||
|                 logger.debug(repr(e)) |                 logger.debug(repr(e)) | ||||||
|   | |||||||
| @@ -54,7 +54,7 @@ class FilesWidget(FileActionSignalsMixin, WindowMixin): | |||||||
|         self.files_view.set_group_name("files_widget") |         self.files_view.set_group_name("files_widget") | ||||||
|         self.builder.expose_object(f"{self.NAME}", self.files_view) |         self.builder.expose_object(f"{self.NAME}", self.files_view) | ||||||
|  |  | ||||||
|     def _load_files_view_state(self, win_name=None, tabs=None): |     def _load_files_view_state(self, win_name = None, tabs = None): | ||||||
|         if win_name == self.NAME: |         if win_name == self.NAME: | ||||||
|             if tabs: |             if tabs: | ||||||
|                 for tab in tabs: |                 for tab in tabs: | ||||||
| @@ -62,9 +62,9 @@ class FilesWidget(FileActionSignalsMixin, WindowMixin): | |||||||
|             else: |             else: | ||||||
|                 self.create_new_tab_notebook(None, self.INDEX, None) |                 self.create_new_tab_notebook(None, self.INDEX, None) | ||||||
|  |  | ||||||
|     def _get_files_view_icon_grid(self, win_index=None, tid=None): |     def _get_files_view_icon_grid(self, win_index = None, tid = None): | ||||||
|         if win_index == str(self.INDEX): |         if win_index == str(self.INDEX): | ||||||
|             return self.builder.get_object(f"{self.INDEX}|{tid}|icon_grid") |             return self.builder.get_object(f"{self.INDEX}|{tid}|icon_grid", use_gtk = False) | ||||||
|  |  | ||||||
|  |  | ||||||
|     def set_fm_controller(self, _fm_controller): |     def set_fm_controller(self, _fm_controller): | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| # Python imports | # Python imports | ||||||
|  | import asyncio | ||||||
|  |  | ||||||
| # Lib imports | # Lib imports | ||||||
| import gi | import gi | ||||||
| @@ -18,7 +19,6 @@ class GridMixin: | |||||||
|     """docstring for GridMixin""" |     """docstring for GridMixin""" | ||||||
|  |  | ||||||
|     def load_store(self, tab, store, save_state = False, use_generator = False): |     def load_store(self, tab, store, save_state = False, use_generator = False): | ||||||
|         store.clear() |  | ||||||
|         dir   = tab.get_current_directory() |         dir   = tab.get_current_directory() | ||||||
|         files = tab.get_files() |         files = tab.get_files() | ||||||
|  |  | ||||||
| @@ -32,35 +32,56 @@ class GridMixin: | |||||||
|             for i, icon in enumerate( self.create_icons_generator(tab, dir, files) ): |             for i, icon in enumerate( self.create_icons_generator(tab, dir, files) ): | ||||||
|                 self.load_icon(i, store, icon) |                 self.load_icon(i, store, icon) | ||||||
|         else: |         else: | ||||||
|             for i, file in enumerate(files): |             # for i, file in enumerate(files): | ||||||
|                 self.create_icon(i, tab, store, dir, file[0]) |             #     self.create_icon(i, tab, store, dir, file[0]) | ||||||
|  |             try: | ||||||
|  |                 loop = asyncio.get_running_loop() | ||||||
|  |             except RuntimeError: | ||||||
|  |                 loop = None | ||||||
|  |  | ||||||
|  |             if loop and loop.is_running(): | ||||||
|  |                 loop.create_task( self.create_icons(tab, store, dir, files) ) | ||||||
|  |             else: | ||||||
|  |                 asyncio.run( self.create_icons(tab, store, dir, files) ) | ||||||
|  |  | ||||||
|         # NOTE: Not likely called often from here but it could be useful |         # NOTE: Not likely called often from here but it could be useful | ||||||
|         if save_state and not trace_debug: |         if save_state and not trace_debug: | ||||||
|             self.fm_controller.save_state() |             self.fm_controller.save_state() | ||||||
|  |  | ||||||
|  |     async def create_icons(self, tab, store, dir, files): | ||||||
|  |         tasks = [self.update_store(i, store, dir, tab, file[0]) for i, file in enumerate(files)] | ||||||
|  |         await asyncio.gather(*tasks) | ||||||
|  |  | ||||||
|  |     async def load_icon(self, i, store, icon): | ||||||
|  |         self.update_store(i, store, icon) | ||||||
|  |  | ||||||
|  |     async def update_store(self, i, store, dir, tab, file): | ||||||
|  |         icon = tab.create_icon(dir, file) | ||||||
|  |         itr  = store.get_iter(i) | ||||||
|  |         store.set_value(itr, 0, icon) | ||||||
|  |  | ||||||
|     def create_icons_generator(self, tab, dir, files): |     def create_icons_generator(self, tab, dir, files): | ||||||
|         for file in files: |         for file in files: | ||||||
|             icon = tab.create_icon(dir, file[0]) |             icon = tab.create_icon(dir, file[0]) | ||||||
|             yield icon |             yield icon | ||||||
|  |  | ||||||
|     @daemon_threaded |     # @daemon_threaded | ||||||
|     def create_icon(self, i, tab, store, dir, file): |     # def create_icon(self, i, tab, store, dir, file): | ||||||
|         icon = tab.create_icon(dir, file) |     #     icon = tab.create_icon(dir, file) | ||||||
|         GLib.idle_add(self.update_store, *(i, store, icon,)) |     #     GLib.idle_add(self.update_store, *(i, store, icon,)) | ||||||
|  |     # | ||||||
|     @daemon_threaded |     # @daemon_threaded | ||||||
|     def load_icon(self, i, store, icon): |     # def load_icon(self, i, store, icon): | ||||||
|         GLib.idle_add(self.update_store, *(i, store, icon,)) |     #     GLib.idle_add(self.update_store, *(i, store, icon,)) | ||||||
|  |     # | ||||||
|     def update_store(self, i, store, icon): |     # def update_store(self, i, store, icon): | ||||||
|         itr = store.get_iter(i) |     #     itr = store.get_iter(i) | ||||||
|         store.set_value(itr, 0, icon) |     #     store.set_value(itr, 0, icon) | ||||||
|  |  | ||||||
|     def create_tab_widget(self, tab): |     def create_tab_widget(self, tab): | ||||||
|         return TabHeaderWidget(tab, self.close_tab) |         return TabHeaderWidget(tab, self.close_tab) | ||||||
|  |  | ||||||
|     def create_scroll_and_store(self, tab, wid, use_tree_view=False): |     def create_scroll_and_store(self, tab, wid, use_tree_view = False): | ||||||
|         scroll = Gtk.ScrolledWindow() |         scroll = Gtk.ScrolledWindow() | ||||||
|  |  | ||||||
|         if not use_tree_view: |         if not use_tree_view: | ||||||
| @@ -72,8 +93,8 @@ class GridMixin: | |||||||
|         scroll.add(grid) |         scroll.add(grid) | ||||||
|         scroll.set_name(f"{wid}|{tab.get_id()}") |         scroll.set_name(f"{wid}|{tab.get_id()}") | ||||||
|         grid.set_name(f"{wid}|{tab.get_id()}") |         grid.set_name(f"{wid}|{tab.get_id()}") | ||||||
|         self.builder.expose_object(f"{wid}|{tab.get_id()}|icon_grid", grid) |         self.builder.expose_object(f"{wid}|{tab.get_id()}|icon_grid", grid, use_gtk = False) | ||||||
|         self.builder.expose_object(f"{wid}|{tab.get_id()}", scroll) |         self.builder.expose_object(f"{wid}|{tab.get_id()}", scroll, use_gtk = False) | ||||||
|  |  | ||||||
|         return scroll, grid.get_store() |         return scroll, grid.get_store() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ from .grid_mixin import GridMixin | |||||||
| class TabMixin(GridMixin): | class TabMixin(GridMixin): | ||||||
|     """docstring for TabMixin""" |     """docstring for TabMixin""" | ||||||
|  |  | ||||||
|     def create_tab(self, wid=None, tid=None, path=None): |     def create_tab(self, wid: int = None, tid: int = None, path: str = None): | ||||||
|         if not wid: |         if not wid: | ||||||
|             wid, tid = self.fm_controller.get_active_wid_and_tid() |             wid, tid = self.fm_controller.get_active_wid_and_tid() | ||||||
|  |  | ||||||
| @@ -62,7 +62,7 @@ class TabMixin(GridMixin): | |||||||
|         tab_box   = button.get_parent() |         tab_box   = button.get_parent() | ||||||
|         wid       = int(notebook.get_name()[-1]) |         wid       = int(notebook.get_name()[-1]) | ||||||
|         tid       = self.get_id_from_tab_box(tab_box) |         tid       = self.get_id_from_tab_box(tab_box) | ||||||
|         scroll    = self.builder.get_object(f"{wid}|{tid}") |         scroll    = self.builder.get_object(f"{wid}|{tid}", use_gtk = False) | ||||||
|         icon_grid = scroll.get_children()[0] |         icon_grid = scroll.get_children()[0] | ||||||
|         store     = icon_grid.get_model() |         store     = icon_grid.get_model() | ||||||
|         tab       = self.get_fm_window(wid).get_tab_by_id(tid) |         tab       = self.get_fm_window(wid).get_tab_by_id(tid) | ||||||
| @@ -71,6 +71,9 @@ class TabMixin(GridMixin): | |||||||
|         watcher.cancel() |         watcher.cancel() | ||||||
|         self.get_fm_window(wid).delete_tab_by_id(tid) |         self.get_fm_window(wid).delete_tab_by_id(tid) | ||||||
|  |  | ||||||
|  |         self.builder.dereference_object(f"{wid}|{tid}|icon_grid") | ||||||
|  |         self.builder.dereference_object(f"{wid}|{tid}") | ||||||
|  |  | ||||||
|         store.clear() |         store.clear() | ||||||
|         # store.run_dispose() |         # store.run_dispose() | ||||||
|         icon_grid.destroy() |         icon_grid.destroy() | ||||||
| @@ -114,7 +117,7 @@ class TabMixin(GridMixin): | |||||||
|         if not settings_manager.is_trace_debug(): |         if not settings_manager.is_trace_debug(): | ||||||
|             self.fm_controller.save_state() |             self.fm_controller.save_state() | ||||||
|  |  | ||||||
|     def on_tab_switch_update(self, notebook, content=None, index=None): |     def on_tab_switch_update(self, notebook, content = None, index = None): | ||||||
|         self.selected_files.clear() |         self.selected_files.clear() | ||||||
|         wid, tid = content.get_children()[0].get_name().split("|") |         wid, tid = content.get_children()[0].get_name().split("|") | ||||||
|         self.fm_controller.set_wid_and_tid(wid, tid) |         self.fm_controller.set_wid_and_tid(wid, tid) | ||||||
| @@ -133,7 +136,7 @@ class TabMixin(GridMixin): | |||||||
|     def get_tab_icon_grid_from_notebook(self, notebook): |     def get_tab_icon_grid_from_notebook(self, notebook): | ||||||
|         return notebook.get_children()[1].get_children()[0] |         return notebook.get_children()[1].get_children()[0] | ||||||
|  |  | ||||||
|     def refresh_tab(data=None): |     def refresh_tab(data = None): | ||||||
|         state = self.get_current_state() |         state = self.get_current_state() | ||||||
|         state.tab.load_directory() |         state.tab.load_directory() | ||||||
|         self.load_store(state.tab, state.store) |         self.load_store(state.tab, state.store) | ||||||
| @@ -150,7 +153,7 @@ class TabMixin(GridMixin): | |||||||
|         if not settings_manager.is_trace_debug(): |         if not settings_manager.is_trace_debug(): | ||||||
|             self.fm_controller.save_state() |             self.fm_controller.save_state() | ||||||
|  |  | ||||||
|     def do_action_from_bar_controls(self, widget, eve=None): |     def do_action_from_bar_controls(self, widget, eve = None): | ||||||
|         action    = widget.get_name() |         action    = widget.get_name() | ||||||
|         wid, tid  = self.fm_controller.get_active_wid_and_tid() |         wid, tid  = self.fm_controller.get_active_wid_and_tid() | ||||||
|         notebook  = self.builder.get_object(f"window_{wid}") |         notebook  = self.builder.get_object(f"window_{wid}") | ||||||
|   | |||||||
| @@ -82,7 +82,7 @@ class Window(Gtk.ApplicationWindow): | |||||||
|         styleContext.add_provider_for_screen(screen, cssProvider, Gtk.STYLE_PROVIDER_PRIORITY_USER) |         styleContext.add_provider_for_screen(screen, cssProvider, Gtk.STYLE_PROVIDER_PRIORITY_USER) | ||||||
|  |  | ||||||
|     def _area_draw(self, widget: Gtk.ApplicationWindow, cr: cairo.Context) -> None: |     def _area_draw(self, widget: Gtk.ApplicationWindow, cr: cairo.Context) -> None: | ||||||
|         cr.set_source_rgba(0, 0, 0, 0.54) |         cr.set_source_rgba( *settings_manager.get_paint_bg_color() ) | ||||||
|         cr.set_operator(cairo.OPERATOR_SOURCE) |         cr.set_operator(cairo.OPERATOR_SOURCE) | ||||||
|         cr.paint() |         cr.paint() | ||||||
|         cr.set_operator(cairo.OPERATOR_OVER) |         cr.set_operator(cairo.OPERATOR_OVER) | ||||||
|   | |||||||
| @@ -50,8 +50,8 @@ class Icon(DesktopIconMixin, VideoIconMixin, MeshsIconMixin): | |||||||
|  |  | ||||||
|             if not thumbnl: |             if not thumbnl: | ||||||
|                 # TODO: Detect if not in a thread and use directly for speed get_system_thumbnail |                 # TODO: Detect if not in a thread and use directly for speed get_system_thumbnail | ||||||
|                 # thumbnl = self.get_system_thumbnail(full_path, self.sys_icon_wh[0]) |                 thumbnl = self.get_system_thumbnail(full_path, self.sys_icon_wh[0]) | ||||||
|                 thumbnl = self._get_system_thumbnail_gtk_thread(full_path, self.sys_icon_wh[0]) |                 # thumbnl = self._get_system_thumbnail_gtk_thread(full_path, self.sys_icon_wh[0]) | ||||||
|                 if not thumbnl: |                 if not thumbnl: | ||||||
|                     raise IconException("No known icons found.") |                     raise IconException("No known icons found.") | ||||||
|  |  | ||||||
| @@ -174,14 +174,15 @@ class Icon(DesktopIconMixin, VideoIconMixin, MeshsIconMixin): | |||||||
|         return path_exists, img_hash, hash_img_path |         return path_exists, img_hash, hash_img_path | ||||||
|  |  | ||||||
|  |  | ||||||
|     def fast_hash(self, filename, hash_factory=hashlib.md5, chunk_num_blocks=128, i=1): |     def fast_hash(self, filename: str, hash_factory: callable = hashlib.md5, chunk_num_blocks: int = 128, i: int = 1) -> str: | ||||||
|         h = hash_factory() |         h = hash_factory() | ||||||
|         with open(filename,'rb') as f: |         with open(filename,'rb') as f: | ||||||
|  |             # NOTE: Jump to middle of file | ||||||
|             f.seek(0, 2) |             f.seek(0, 2) | ||||||
|             mid = int(f.tell() / 2) |             mid = int(f.tell() / 2) | ||||||
|             f.seek(mid, 0) |             f.seek(mid, 0) | ||||||
|  |  | ||||||
|             while chunk := f.read(chunk_num_blocks*h.block_size): |             while chunk := f.read(chunk_num_blocks * h.block_size): | ||||||
|                 h.update(chunk) |                 h.update(chunk) | ||||||
|                 if (i == 12): |                 if (i == 12): | ||||||
|                     break |                     break | ||||||
|   | |||||||
| @@ -68,7 +68,7 @@ class IPCServer(Singleton): | |||||||
|     def _handle_ipc_message(self, conn, start_time) -> None: |     def _handle_ipc_message(self, conn, start_time) -> None: | ||||||
|         while True: |         while True: | ||||||
|             msg = conn.recv() |             msg = conn.recv() | ||||||
|             if settings.is_debug(): |             if settings_manager.is_debug(): | ||||||
|                 print(msg) |                 print(msg) | ||||||
|  |  | ||||||
|             if "FILE|" in msg: |             if "FILE|" in msg: | ||||||
|   | |||||||
| @@ -35,7 +35,7 @@ class SettingsManager(StartCheckMixin, Singleton): | |||||||
|         self._CSS_FILE          = f"{self._HOME_CONFIG_PATH}/stylesheet.css" |         self._CSS_FILE          = f"{self._HOME_CONFIG_PATH}/stylesheet.css" | ||||||
|         self._KEY_BINDINGS_FILE = f"{self._HOME_CONFIG_PATH}/key-bindings.json" |         self._KEY_BINDINGS_FILE = f"{self._HOME_CONFIG_PATH}/key-bindings.json" | ||||||
|         self._PID_FILE          = f"{self._HOME_CONFIG_PATH}/{app_name.lower()}.pid" |         self._PID_FILE          = f"{self._HOME_CONFIG_PATH}/{app_name.lower()}.pid" | ||||||
|         self._WINDOW_ICON       = f"{self._DEFAULT_ICONS}/icons/{app_name.lower()}.png" |         self._WINDOW_ICON       = f"{self._DEFAULT_ICONS}/{app_name.lower()}.png" | ||||||
|         self._UI_WIDEGTS_PATH   = f"{self._HOME_CONFIG_PATH}/ui_widgets" |         self._UI_WIDEGTS_PATH   = f"{self._HOME_CONFIG_PATH}/ui_widgets" | ||||||
|         self._CONTEXT_MENU      = f"{self._HOME_CONFIG_PATH}/contexct_menu.json" |         self._CONTEXT_MENU      = f"{self._HOME_CONFIG_PATH}/contexct_menu.json" | ||||||
|         self._TRASH_FILES_PATH  = f"{GLib.get_user_data_dir()}/Trash/files" |         self._TRASH_FILES_PATH  = f"{GLib.get_user_data_dir()}/Trash/files" | ||||||
| @@ -92,6 +92,7 @@ class SettingsManager(StartCheckMixin, Singleton): | |||||||
|         self._main_window_w     = 1670 |         self._main_window_w     = 1670 | ||||||
|         self._main_window_h     = 830 |         self._main_window_h     = 830 | ||||||
|         self._builder           = None |         self._builder           = None | ||||||
|  |         self.PAINT_BG_COLOR     = (0, 0, 0, 0.0) | ||||||
|  |  | ||||||
|         self._trace_debug       = False |         self._trace_debug       = False | ||||||
|         self._debug             = False |         self._debug             = False | ||||||
| @@ -127,6 +128,8 @@ class SettingsManager(StartCheckMixin, Singleton): | |||||||
|     def get_main_window_width(self)  -> Gtk.ApplicationWindow: return self._main_window_w |     def get_main_window_width(self)  -> Gtk.ApplicationWindow: return self._main_window_w | ||||||
|     def get_main_window_height(self) -> Gtk.ApplicationWindow: return self._main_window_h |     def get_main_window_height(self) -> Gtk.ApplicationWindow: return self._main_window_h | ||||||
|     def get_builder(self)            -> Gtk.Builder:           return self._builder |     def get_builder(self)            -> Gtk.Builder:           return self._builder | ||||||
|  |     def get_paint_bg_color(self)     -> list:                  return self.PAINT_BG_COLOR | ||||||
|  |  | ||||||
|     def get_glade_file(self)        -> str: return self._GLADE_FILE |     def get_glade_file(self)        -> str: return self._GLADE_FILE | ||||||
|     def get_icon_theme(self)        -> str: return self._ICON_THEME |     def get_icon_theme(self)        -> str: return self._ICON_THEME | ||||||
|     def get_css_file(self)          -> str: return self._CSS_FILE |     def get_css_file(self)          -> str: return self._CSS_FILE | ||||||
|   | |||||||
							
								
								
									
										45
									
								
								user_config/usr/share/solarfm/settings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								user_config/usr/share/solarfm/settings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | { | ||||||
|  |     "config": { | ||||||
|  |         "base_of_home": "", | ||||||
|  |         "hide_hidden_files": "true", | ||||||
|  |         "thumbnailer_path": "ffmpegthumbnailer", | ||||||
|  |         "blender_thumbnailer_path": "", | ||||||
|  |         "go_past_home": "true", | ||||||
|  |         "lock_folder": "false", | ||||||
|  |         "locked_folders": "venv::::flasks", | ||||||
|  |         "mplayer_options": "-quiet -really-quiet -xy 1600 -geometry 50%:50%", | ||||||
|  |         "music_app": "deadbeef", | ||||||
|  |         "media_app": "mpv", | ||||||
|  |         "image_app": "mirage2", | ||||||
|  |         "office_app": "libreoffice", | ||||||
|  |         "pdf_app": "evince", | ||||||
|  |         "code_app": "newton", | ||||||
|  |         "text_app": "mousepad", | ||||||
|  |         "terminal_app": "terminator", | ||||||
|  |         "container_icon_wh": [128, 128], | ||||||
|  |         "video_icon_wh":     [128, 64], | ||||||
|  |         "sys_icon_wh":       [56, 56], | ||||||
|  |         "file_manager_app": "solarfm", | ||||||
|  |         "steam_cdn_url": "https://steamcdn-a.akamaihd.net/steam/apps/", | ||||||
|  |         "remux_folder_max_disk_usage": "8589934592" | ||||||
|  |     }, | ||||||
|  |     "filters": { | ||||||
|  |         "meshs":  [".dae", ".fbx", ".gltf", ".obj", ".stl"], | ||||||
|  |         "code":   [".cpp", ".css", ".c", ".go", ".html", ".htm", ".java", ".js", ".json", ".lua", ".md", ".py", ".rs", ".toml", ".xml", ".pom"], | ||||||
|  |         "videos": [".mkv", ".mp4", ".webm", ".avi", ".mov", ".m4v", ".mpg", ".mpeg", ".wmv", ".flv"], | ||||||
|  |         "office": [".doc", ".docx", ".xls", ".xlsx", ".xlt", ".xltx", ".xlm", ".ppt", ".pptx", ".pps", ".ppsx", ".odt", ".rtf"], | ||||||
|  |         "images": [".png", ".jpg", ".jpeg", ".gif", ".ico", ".tga", ".webp"], | ||||||
|  |         "text":   [".txt", ".text", ".sh", ".cfg", ".conf", ".log"], | ||||||
|  |         "music":  [".psf", ".mp3", ".ogg", ".flac", ".m4a"], | ||||||
|  |         "pdf":    [".pdf"] | ||||||
|  |    }, | ||||||
|  |    "theming":{ | ||||||
|  |        "success_color": "#88cc27", | ||||||
|  |        "warning_color": "#ffa800", | ||||||
|  |        "error_color":   "#ff0000" | ||||||
|  |    }, | ||||||
|  |    "debugging": { | ||||||
|  |        "ch_log_lvl": 20, | ||||||
|  |        "fh_log_lvl": 10 | ||||||
|  |    } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user