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> | ||||
| ``` | ||||
| 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 | ||||
|   | ||||
							
								
								
									
										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,7 +77,6 @@ class Controller(UIMixin, SignalsMixins, Controller_Data): | ||||
|         event_system.subscribe("set_clipboard_data", self.set_clipboard_data) | ||||
|  | ||||
|     def _load_glade_file(self): | ||||
|         self.builder = Gtk.Builder() | ||||
|         self.builder.add_from_file( settings_manager.get_glade_file() ) | ||||
|         self.builder.expose_object("main_window", self.window) | ||||
|  | ||||
|   | ||||
| @@ -35,13 +35,39 @@ class State: | ||||
|     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: | ||||
|     """ 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" | ||||
|  | ||||
|     def _setup_controller_data(self) -> None: | ||||
|         self.window        = settings_manager.get_main_window() | ||||
|         self.builder       = None | ||||
|         self.builder       = SFMBuilder() | ||||
|         self.core_widget   = None | ||||
|  | ||||
|         self._load_glade_file() | ||||
| @@ -88,7 +114,7 @@ class Controller_Data: | ||||
|         state.notebooks        = self.notebooks | ||||
|         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.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.store            = state.icon_grid.get_model() | ||||
|         state.message_dialog   = MessageWidget() | ||||
|   | ||||
| @@ -68,7 +68,7 @@ class FileActionSignalsMixin: | ||||
|         wid, tid  = tab_widget.split("|") | ||||
|         notebook  = self.builder.get_object(f"window_{wid}") | ||||
|         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, tab_widget_label = self.get_store_and_label_from_notebook(notebook, f"{wid}|{tid}") | ||||
|  | ||||
|   | ||||
| @@ -74,7 +74,7 @@ class KeyboardSignalsMixin: | ||||
|     def keyboard_close_tab(self): | ||||
|         wid, tid  = self.fm_controller.get_active_wid_and_tid() | ||||
|         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) | ||||
|         tab       = self.get_fm_window(wid).get_tab_by_id(tid) | ||||
|         watcher   = tab.get_dir_watcher() | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| # Python imports | ||||
| import asyncio | ||||
|  | ||||
| # Lib imports | ||||
| import gi | ||||
| @@ -31,30 +32,51 @@ class GridMixin: | ||||
|             for i, icon in enumerate( self.create_icons_generator(tab, dir, files) ): | ||||
|                 self.load_icon(i, store, icon) | ||||
|         else: | ||||
|             for i, file in enumerate(files): | ||||
|                 self.create_icon(i, tab, store, dir, file[0]) | ||||
|             # for i, file in enumerate(files): | ||||
|             #     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 | ||||
|         if save_state and not trace_debug: | ||||
|             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): | ||||
|         for file in files: | ||||
|             icon = tab.create_icon(dir, file[0]) | ||||
|             yield icon | ||||
|  | ||||
|     @daemon_threaded | ||||
|     def create_icon(self, i, tab, store, dir, file): | ||||
|         icon = tab.create_icon(dir, file) | ||||
|         GLib.idle_add(self.update_store, *(i, store, icon,)) | ||||
|     # @daemon_threaded | ||||
|     # def create_icon(self, i, tab, store, dir, file): | ||||
|     #     icon = tab.create_icon(dir, file) | ||||
|     #     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 load_icon(self, i, store, icon): | ||||
|         GLib.idle_add(self.update_store, *(i, store, icon,)) | ||||
|  | ||||
|     def update_store(self, i, store, icon): | ||||
|         itr = store.get_iter(i) | ||||
|         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): | ||||
|         return TabHeaderWidget(tab, self.close_tab) | ||||
| @@ -71,8 +93,8 @@ class GridMixin: | ||||
|         scroll.add(grid) | ||||
|         scroll.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()}", scroll) | ||||
|         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, use_gtk = False) | ||||
|  | ||||
|         return scroll, grid.get_store() | ||||
|  | ||||
|   | ||||
| @@ -17,7 +17,7 @@ from .grid_mixin import GridMixin | ||||
| class TabMixin(GridMixin): | ||||
|     """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: | ||||
|             wid, tid = self.fm_controller.get_active_wid_and_tid() | ||||
|  | ||||
| @@ -60,7 +60,7 @@ class TabMixin(GridMixin): | ||||
|         tab_box   = button.get_parent() | ||||
|         wid       = int(notebook.get_name()[-1]) | ||||
|         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] | ||||
|         store     = icon_grid.get_store() | ||||
|         tab       = self.get_fm_window(wid).get_tab_by_id(tid) | ||||
| @@ -69,6 +69,9 @@ class TabMixin(GridMixin): | ||||
|         watcher.cancel() | ||||
|         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.run_dispose() | ||||
|         icon_grid.destroy() | ||||
|   | ||||
| @@ -36,6 +36,15 @@ class UIMixin(PaneMixin, WindowMixin): | ||||
|             isHidden = True if session["window"]["isHidden"] == "True" else False | ||||
|             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): | ||||
|         if session_json: | ||||
| @@ -58,16 +67,17 @@ class UIMixin(PaneMixin, WindowMixin): | ||||
|  | ||||
|             try: | ||||
|                 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: | ||||
|                     icon_grid = self.window3.get_children()[-1].get_children()[0] | ||||
|                     notebook = self.window3 | ||||
|                 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: | ||||
|                     icon_grid = self.window1.get_children()[-1].get_children()[0] | ||||
|                     notebook = self.window1 | ||||
|  | ||||
|                 icon_grid.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE)) | ||||
|                 icon_grid.event(Gdk.Event().new(type=Gdk.EventType.BUTTON_RELEASE)) | ||||
|                 scroll_win = notebook.get_children()[-1] | ||||
|                 icon_grid  = scroll_win.get_children()[0] | ||||
|                 self._focus_last_visible_notebook(icon_grid) | ||||
|             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.debug(repr(e)) | ||||
|   | ||||
| @@ -64,7 +64,7 @@ class FilesWidget(FileActionSignalsMixin, WindowMixin): | ||||
|  | ||||
|     def _get_files_view_icon_grid(self, win_index = None, tid = None): | ||||
|         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): | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| # Python imports | ||||
| import asyncio | ||||
|  | ||||
| # Lib imports | ||||
| import gi | ||||
| @@ -18,7 +19,6 @@ class GridMixin: | ||||
|     """docstring for GridMixin""" | ||||
|  | ||||
|     def load_store(self, tab, store, save_state = False, use_generator = False): | ||||
|         store.clear() | ||||
|         dir   = tab.get_current_directory() | ||||
|         files = tab.get_files() | ||||
|  | ||||
| @@ -32,30 +32,51 @@ class GridMixin: | ||||
|             for i, icon in enumerate( self.create_icons_generator(tab, dir, files) ): | ||||
|                 self.load_icon(i, store, icon) | ||||
|         else: | ||||
|             for i, file in enumerate(files): | ||||
|                 self.create_icon(i, tab, store, dir, file[0]) | ||||
|             # for i, file in enumerate(files): | ||||
|             #     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 | ||||
|         if save_state and not trace_debug: | ||||
|             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): | ||||
|         for file in files: | ||||
|             icon = tab.create_icon(dir, file[0]) | ||||
|             yield icon | ||||
|  | ||||
|     @daemon_threaded | ||||
|     def create_icon(self, i, tab, store, dir, file): | ||||
|         icon = tab.create_icon(dir, file) | ||||
|         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,)) | ||||
|  | ||||
|     def update_store(self, i, store, icon): | ||||
|         itr = store.get_iter(i) | ||||
|         store.set_value(itr, 0, icon) | ||||
|     # @daemon_threaded | ||||
|     # def create_icon(self, i, tab, store, dir, file): | ||||
|     #     icon = tab.create_icon(dir, file) | ||||
|     #     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,)) | ||||
|     # | ||||
|     # def update_store(self, i, store, icon): | ||||
|     #     itr = store.get_iter(i) | ||||
|     #     store.set_value(itr, 0, icon) | ||||
|  | ||||
|     def create_tab_widget(self, tab): | ||||
|         return TabHeaderWidget(tab, self.close_tab) | ||||
| @@ -72,8 +93,8 @@ class GridMixin: | ||||
|         scroll.add(grid) | ||||
|         scroll.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()}", scroll) | ||||
|         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, use_gtk = False) | ||||
|  | ||||
|         return scroll, grid.get_store() | ||||
|  | ||||
|   | ||||
| @@ -17,7 +17,7 @@ from .grid_mixin import GridMixin | ||||
| class TabMixin(GridMixin): | ||||
|     """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: | ||||
|             wid, tid = self.fm_controller.get_active_wid_and_tid() | ||||
|  | ||||
| @@ -62,7 +62,7 @@ class TabMixin(GridMixin): | ||||
|         tab_box   = button.get_parent() | ||||
|         wid       = int(notebook.get_name()[-1]) | ||||
|         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] | ||||
|         store     = icon_grid.get_model() | ||||
|         tab       = self.get_fm_window(wid).get_tab_by_id(tid) | ||||
| @@ -71,6 +71,9 @@ class TabMixin(GridMixin): | ||||
|         watcher.cancel() | ||||
|         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.run_dispose() | ||||
|         icon_grid.destroy() | ||||
|   | ||||
| @@ -82,7 +82,7 @@ class Window(Gtk.ApplicationWindow): | ||||
|         styleContext.add_provider_for_screen(screen, cssProvider, Gtk.STYLE_PROVIDER_PRIORITY_USER) | ||||
|  | ||||
|     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.paint() | ||||
|         cr.set_operator(cairo.OPERATOR_OVER) | ||||
|   | ||||
| @@ -50,8 +50,8 @@ class Icon(DesktopIconMixin, VideoIconMixin, MeshsIconMixin): | ||||
|  | ||||
|             if not thumbnl: | ||||
|                 # 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_gtk_thread(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]) | ||||
|                 if not thumbnl: | ||||
|                     raise IconException("No known icons found.") | ||||
|  | ||||
| @@ -174,9 +174,10 @@ class Icon(DesktopIconMixin, VideoIconMixin, MeshsIconMixin): | ||||
|         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() | ||||
|         with open(filename,'rb') as f: | ||||
|             # NOTE: Jump to middle of file | ||||
|             f.seek(0, 2) | ||||
|             mid = int(f.tell() / 2) | ||||
|             f.seek(mid, 0) | ||||
|   | ||||
| @@ -68,7 +68,7 @@ class IPCServer(Singleton): | ||||
|     def _handle_ipc_message(self, conn, start_time) -> None: | ||||
|         while True: | ||||
|             msg = conn.recv() | ||||
|             if settings.is_debug(): | ||||
|             if settings_manager.is_debug(): | ||||
|                 print(msg) | ||||
|  | ||||
|             if "FILE|" in msg: | ||||
|   | ||||
| @@ -35,7 +35,7 @@ class SettingsManager(StartCheckMixin, Singleton): | ||||
|         self._CSS_FILE          = f"{self._HOME_CONFIG_PATH}/stylesheet.css" | ||||
|         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._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._CONTEXT_MENU      = f"{self._HOME_CONFIG_PATH}/contexct_menu.json" | ||||
|         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_h     = 830 | ||||
|         self._builder           = None | ||||
|         self.PAINT_BG_COLOR     = (0, 0, 0, 0.0) | ||||
|  | ||||
|         self._trace_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_height(self) -> Gtk.ApplicationWindow: return self._main_window_h | ||||
|     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_icon_theme(self)        -> str: return self._ICON_THEME | ||||
|     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