diff --git a/README.md b/README.md index 9f0ca6a..d796660 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Copy the share/solarfm folder to your user .config/ directory too.
Install Setup
``` -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 @@ -32,4 +32,4 @@ A selected file in the active quad-pane will move to trash since it is the defau ![1 SolarFM single pane. ](images/pic1.png) ![2 SolarFM double pane. ](images/pic2.png) ![3 SolarFM triple pane. ](images/pic3.png) -![4 SolarFM quad pane. ](images/pic4.png) +![4 SolarFM quad pane. ](images/pic4.png) \ No newline at end of file diff --git a/plugins/movie_tv_info/tmdbscraper/lib/__init__.py b/plugins/movie_tv_info/tmdbscraper/lib/__init__.py new file mode 100644 index 0000000..d36fa8c --- /dev/null +++ b/plugins/movie_tv_info/tmdbscraper/lib/__init__.py @@ -0,0 +1,3 @@ +""" + Pligin Module +""" diff --git a/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/__init__.py b/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/__init__.py new file mode 100644 index 0000000..f10f263 --- /dev/null +++ b/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/__init__.py @@ -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 +} diff --git a/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/api_utils.py b/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/api_utils.py new file mode 100644 index 0000000..31fe215 --- /dev/null +++ b/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/api_utils.py @@ -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 . + +"""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 diff --git a/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/fanarttv.py b/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/fanarttv.py new file mode 100644 index 0000000..f19cb1c --- /dev/null +++ b/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/fanarttv.py @@ -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 diff --git a/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/imdbratings.py b/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/imdbratings.py new file mode 100644 index 0000000..eba96b1 --- /dev/null +++ b/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/imdbratings.py @@ -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 . +# +# 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 diff --git a/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/tmdb.py b/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/tmdb.py new file mode 100644 index 0000000..a7f4082 --- /dev/null +++ b/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/tmdb.py @@ -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 diff --git a/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/tmdbapi.py b/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/tmdbapi.py new file mode 100644 index 0000000..1fda4b2 --- /dev/null +++ b/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/tmdbapi.py @@ -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 . +# 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 diff --git a/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/traktratings.py b/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/traktratings.py new file mode 100644 index 0000000..7e24d5e --- /dev/null +++ b/plugins/movie_tv_info/tmdbscraper/lib/tmdbscraper/traktratings.py @@ -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 . +# 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 diff --git a/plugins/translate/brotli/_brotli.abi3.so b/plugins/translate/brotli/_brotli.abi3.so new file mode 100755 index 0000000..d1caea2 Binary files /dev/null and b/plugins/translate/brotli/_brotli.abi3.so differ diff --git a/src/versions/solarfm-0.0.1/solarfm/..o b/src/versions/solarfm-0.0.1/solarfm/..o deleted file mode 100644 index 878e30f..0000000 Binary files a/src/versions/solarfm-0.0.1/solarfm/..o and /dev/null differ diff --git a/src/versions/solarfm-0.0.1/solarfm/core/controller.py b/src/versions/solarfm-0.0.1/solarfm/core/controller.py index 21e9282..b07fb5f 100644 --- a/src/versions/solarfm-0.0.1/solarfm/core/controller.py +++ b/src/versions/solarfm-0.0.1/solarfm/core/controller.py @@ -77,8 +77,7 @@ 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.add_from_file( settings_manager.get_glade_file() ) self.builder.expose_object("main_window", self.window) self.core_widget = self.builder.get_object("core_widget") diff --git a/src/versions/solarfm-0.0.1/solarfm/core/controller_data.py b/src/versions/solarfm-0.0.1/solarfm/core/controller_data.py index 083c69a..2605c64 100644 --- a/src/versions/solarfm-0.0.1/solarfm/core/controller_data.py +++ b/src/versions/solarfm-0.0.1/solarfm/core/controller_data.py @@ -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() @@ -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 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) dir = tab.get_current_directory() uris = [] diff --git a/src/versions/solarfm-0.0.1/solarfm/core/mixins/signals/file_action_signals_mixin.py b/src/versions/solarfm-0.0.1/solarfm/core/mixins/signals/file_action_signals_mixin.py index ec2bd33..aba1eac 100644 --- a/src/versions/solarfm-0.0.1/solarfm/core/mixins/signals/file_action_signals_mixin.py +++ b/src/versions/solarfm-0.0.1/solarfm/core/mixins/signals/file_action_signals_mixin.py @@ -35,7 +35,7 @@ class FileActionSignalsMixin: # 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 - 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, Gio.FileMonitorEvent.RENAMED, Gio.FileMonitorEvent.MOVED_IN, Gio.FileMonitorEvent.MOVED_OUT]: @@ -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}") @@ -82,7 +82,7 @@ class FileActionSignalsMixin: 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: target = widget.get_name() notebook = self.builder.get_object(target) diff --git a/src/versions/solarfm-0.0.1/solarfm/core/mixins/signals/keyboard_signals_mixin.py b/src/versions/solarfm-0.0.1/solarfm/core/mixins/signals/keyboard_signals_mixin.py index 62508ab..9e02fe1 100644 --- a/src/versions/solarfm-0.0.1/solarfm/core/mixins/signals/keyboard_signals_mixin.py +++ b/src/versions/solarfm-0.0.1/solarfm/core/mixins/signals/keyboard_signals_mixin.py @@ -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() diff --git a/src/versions/solarfm-0.0.1/solarfm/core/mixins/ui/grid_mixin.py b/src/versions/solarfm-0.0.1/solarfm/core/mixins/ui/grid_mixin.py index 3d31316..85772b3 100644 --- a/src/versions/solarfm-0.0.1/solarfm/core/mixins/ui/grid_mixin.py +++ b/src/versions/solarfm-0.0.1/solarfm/core/mixins/ui/grid_mixin.py @@ -1,4 +1,5 @@ # Python imports +import asyncio # Lib imports import gi @@ -31,35 +32,56 @@ 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) - 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() if not use_tree_view: @@ -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() diff --git a/src/versions/solarfm-0.0.1/solarfm/core/mixins/ui/tab_mixin.py b/src/versions/solarfm-0.0.1/solarfm/core/mixins/ui/tab_mixin.py index e88f393..ed79fcd 100644 --- a/src/versions/solarfm-0.0.1/solarfm/core/mixins/ui/tab_mixin.py +++ b/src/versions/solarfm-0.0.1/solarfm/core/mixins/ui/tab_mixin.py @@ -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() @@ -112,7 +115,7 @@ class TabMixin(GridMixin): if not settings_manager.is_trace_debug(): 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() wid, tid = content.get_children()[0].get_name().split("|") 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): return notebook.get_children()[1].get_children()[0] - def refresh_tab(data=None): + def refresh_tab(data = None): state = self.get_current_state() state.tab.load_directory() self.load_store(state.tab, state.store) @@ -148,7 +151,7 @@ class TabMixin(GridMixin): if not settings_manager.is_trace_debug(): 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() wid, tid = self.fm_controller.get_active_wid_and_tid() notebook = self.builder.get_object(f"window_{wid}") diff --git a/src/versions/solarfm-0.0.1/solarfm/core/ui_mixin.py b/src/versions/solarfm-0.0.1/solarfm/core/ui_mixin.py index 663ae4a..4c44c33 100644 --- a/src/versions/solarfm-0.0.1/solarfm/core/ui_mixin.py +++ b/src/versions/solarfm-0.0.1/solarfm/core/ui_mixin.py @@ -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)) diff --git a/src/versions/solarfm-0.0.1/solarfm/core/widgets/files_view/files_widget.py b/src/versions/solarfm-0.0.1/solarfm/core/widgets/files_view/files_widget.py index 9a49117..cca26d6 100644 --- a/src/versions/solarfm-0.0.1/solarfm/core/widgets/files_view/files_widget.py +++ b/src/versions/solarfm-0.0.1/solarfm/core/widgets/files_view/files_widget.py @@ -54,7 +54,7 @@ class FilesWidget(FileActionSignalsMixin, WindowMixin): self.files_view.set_group_name("files_widget") 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 tabs: for tab in tabs: @@ -62,9 +62,9 @@ class FilesWidget(FileActionSignalsMixin, WindowMixin): else: 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): - 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): diff --git a/src/versions/solarfm-0.0.1/solarfm/core/widgets/files_view/grid_mixin.py b/src/versions/solarfm-0.0.1/solarfm/core/widgets/files_view/grid_mixin.py index 2725424..0b02715 100644 --- a/src/versions/solarfm-0.0.1/solarfm/core/widgets/files_view/grid_mixin.py +++ b/src/versions/solarfm-0.0.1/solarfm/core/widgets/files_view/grid_mixin.py @@ -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,35 +32,56 @@ 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) - 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() if not use_tree_view: @@ -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() diff --git a/src/versions/solarfm-0.0.1/solarfm/core/widgets/files_view/tab_mixin.py b/src/versions/solarfm-0.0.1/solarfm/core/widgets/files_view/tab_mixin.py index 1bf3b50..9ea81ba 100644 --- a/src/versions/solarfm-0.0.1/solarfm/core/widgets/files_view/tab_mixin.py +++ b/src/versions/solarfm-0.0.1/solarfm/core/widgets/files_view/tab_mixin.py @@ -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() @@ -114,7 +117,7 @@ class TabMixin(GridMixin): if not settings_manager.is_trace_debug(): 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() wid, tid = content.get_children()[0].get_name().split("|") 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): return notebook.get_children()[1].get_children()[0] - def refresh_tab(data=None): + def refresh_tab(data = None): state = self.get_current_state() state.tab.load_directory() self.load_store(state.tab, state.store) @@ -150,7 +153,7 @@ class TabMixin(GridMixin): if not settings_manager.is_trace_debug(): 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() wid, tid = self.fm_controller.get_active_wid_and_tid() notebook = self.builder.get_object(f"window_{wid}") diff --git a/src/versions/solarfm-0.0.1/solarfm/core/window.py b/src/versions/solarfm-0.0.1/solarfm/core/window.py index 5960f10..8829ef8 100644 --- a/src/versions/solarfm-0.0.1/solarfm/core/window.py +++ b/src/versions/solarfm-0.0.1/solarfm/core/window.py @@ -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) diff --git a/src/versions/solarfm-0.0.1/solarfm/shellfm/windows/tabs/icons/icon.py b/src/versions/solarfm-0.0.1/solarfm/shellfm/windows/tabs/icons/icon.py index c5330b5..72285b0 100644 --- a/src/versions/solarfm-0.0.1/solarfm/shellfm/windows/tabs/icons/icon.py +++ b/src/versions/solarfm-0.0.1/solarfm/shellfm/windows/tabs/icons/icon.py @@ -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,14 +174,15 @@ 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) - while chunk := f.read(chunk_num_blocks*h.block_size): + while chunk := f.read(chunk_num_blocks * h.block_size): h.update(chunk) if (i == 12): break diff --git a/src/versions/solarfm-0.0.1/solarfm/utils/ipc_server.py b/src/versions/solarfm-0.0.1/solarfm/utils/ipc_server.py index 6972112..563b086 100644 --- a/src/versions/solarfm-0.0.1/solarfm/utils/ipc_server.py +++ b/src/versions/solarfm-0.0.1/solarfm/utils/ipc_server.py @@ -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: @@ -105,4 +105,4 @@ class IPCServer(Singleton): except ConnectionRefusedError as e: print("Connection refused...") except Exception as e: - print(repr(e)) + print(repr(e)) \ No newline at end of file diff --git a/src/versions/solarfm-0.0.1/solarfm/utils/settings_manager/manager.py b/src/versions/solarfm-0.0.1/solarfm/utils/settings_manager/manager.py index 8604ef8..4730741 100644 --- a/src/versions/solarfm-0.0.1/solarfm/utils/settings_manager/manager.py +++ b/src/versions/solarfm-0.0.1/solarfm/utils/settings_manager/manager.py @@ -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 @@ -126,7 +127,9 @@ class SettingsManager(StartCheckMixin, Singleton): def get_main_window(self) -> Gtk.ApplicationWindow: return self._main_window 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_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 @@ -161,4 +164,4 @@ class SettingsManager(StartCheckMixin, Singleton): def save_settings(self): with open(self._CONFIG_FILE, 'w') as outfile: - json.dump(self.settings.as_dict(), outfile, separators=(',', ':'), indent=4) + json.dump(self.settings.as_dict(), outfile, separators=(',', ':'), indent=4) \ No newline at end of file diff --git a/user_config/usr/share/solarfm/settings.json b/user_config/usr/share/solarfm/settings.json new file mode 100644 index 0000000..a67c142 --- /dev/null +++ b/user_config/usr/share/solarfm/settings.json @@ -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 + } +} \ No newline at end of file