diff --git a/addon.xml b/addon.xml index 6be773e..ea08e3c 100644 --- a/addon.xml +++ b/addon.xml @@ -1,19 +1,22 @@ - + - + + + + - audio video + video - assorted collection of mostly electronic music videos + curated collection of mostly electronic music videos Add-on to play videos listed on https://vidfltr.slashproc.org A few videos don't work in every country - assorted collection of mostly electronic music videos + curated collection of mostly electronic music videos Add-on für https://vidfltr.slashproc.org Ein Paar Videos funktionieren nicht in allen Ländern @@ -30,7 +33,9 @@ resources/img/kodi/clearlogo.png - 0.9.9 + 1.0.0-alpha.1 +- finally push an initial 1.0.0-rc.1 with all the additions and fixes added in the last years +0.9.9 - play with new Vimeo Add-on from jaylinski if it's available and otherwise fallback to youtube-dl 0.9.8 - add folder display settings @@ -59,6 +64,6 @@ - New: "Similar Music" context menu (generated with Musly) 0.9.1 - Inital Release - + diff --git a/changelog.txt b/changelog.txt index 6acc40e..dd33f00 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,5 @@ +1.0.0-alpha.1 +- finally push an initial 1.0.0-rc.1 with all the additions and fixes added in the last years 0.9.9 - play with new Vimeo Add-on from jaylinski if it's available and otherwise fallback to youtube-dl 0.9.8 diff --git a/default.py b/default.py index 9d565a3..258f9ee 100644 --- a/default.py +++ b/default.py @@ -1,36 +1,92 @@ # -*- coding: utf-8 -*- -from __future__ import unicode_literals + + +# from __future__ import unicode_literals from kodi_six import xbmc, xbmcaddon, xbmcgui, xbmcplugin -import sys -import os -import urllib -import json + +try: + from xbmcvfs import translatePath as xbmcTranslatePath +except ImportError: + from xbmc import translatePath as xbmcTranslatePath + +import gzip import io -import re -import time -from datetime import date, datetime, timedelta -import random +import json import math +import os +import random +import re +import sys +import time +import ast +from datetime import date, datetime, timedelta +from time import localtime, strftime + +from six.moves import urllib_parse +from six.moves.urllib.parse import quote, quote_plus from unidecode import unidecode -addonID = 'plugin.video.vidfltr' +try: + import urllib + + target = urllib.URLopener() +except AttributeError: + # Python 3 + import urllib.request + + target = urllib.request.URLopener() + +addon = xbmcaddon.Addon() +addon_name = addon.getAddonInfo("name") +addon_version = addon.getAddonInfo("version") + +addonID = "plugin.video.vidfltr" addon = xbmcaddon.Addon(id=addonID) pluginhandle = int(sys.argv[1]) translation = addon.getLocalizedString -addonDir = xbmc.translatePath(addon.getAddonInfo('path')) -defaultFanart = os.path.join(addonDir, 'resources/img/kodi/noicon.png') -icon = os.path.join(addonDir, 'resources/img/kodi/icon.png') -clearlogo = os.path.join(addonDir, 'resources/img/kodi/clearlogo.png') -fanart = os.path.join(addonDir, 'resources/img/kodi/fanart.png') -banner = os.path.join(addonDir, 'resources/img/kodi/banner.png') -poster = os.path.join(addonDir, 'resources/img/kodi/poster.png') -addon_work_folder = xbmc.translatePath("special://profile/addon_data/" + addonID) -jsonVideos = xbmc.translatePath("special://profile/addon_data/" + addonID + "/videos.json") -jsonArtists = xbmc.translatePath("special://profile/addon_data/" + addonID + "/artists.json") -jsonStyles = xbmc.translatePath("special://profile/addon_data/" + addonID + "/styles.json") +xbmcversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2]) + +addonDir = xbmcTranslatePath(addon.getAddonInfo("path")) +addonAuthor = xbmcTranslatePath(addon.getAddonInfo("author")) + +addon_work_folder = xbmcTranslatePath("special://profile/addon_data/" + addonID) + + +if str(addon.getSetting("debugvscode")) == "true": + # we have no psutil on CoreELEC Kodi so we kill the debugging process the ugly way + import os + import signal + import subprocess + + p = subprocess.Popen(["ps", "a"], stdout=subprocess.PIPE) + out, err = p.communicate() + for line in out.splitlines(): + if "script.module.debugpy" in str(line): + pid = int(line.split(None, 1)[0]) + os.kill(pid, signal.SIGKILL) + # start debugger + import debugpy + debugpy.listen(("localhost", 5678)) + debugpy.wait_for_client() + +jsonVideos = xbmcTranslatePath( + "special://profile/addon_data/" + addonID + "/videos.json.gz" +) +jsonArtists = xbmcTranslatePath( + "special://profile/addon_data/" + addonID + "/artists.json.gz" +) +jsonStyles = xbmcTranslatePath( + "special://profile/addon_data/" + addonID + "/styles.json.gz" +) + +defaultFanart = os.path.join(addonDir, "resources/img/kodi/noicon.png") +icon = os.path.join(addonDir, "resources/img/kodi/icon.png") +clearlogo = os.path.join(addonDir, "resources/img/kodi/clearlogo.png") +fanart = os.path.join(addonDir, "resources/img/kodi/fanart.jpg") +banner = os.path.join(addonDir, "resources/img/kodi/banner.jpg") +poster = os.path.join(addonDir, "resources/img/kodi/poster.jpg") maxFileAge = int(addon.getSetting("maxFileAge")) maxFileAge = maxFileAge * 60 -mediatype = addon.getSetting("mediatype") # show only official, fanmade or all videos? videoselection = str(addon.getSetting("videoselection")).lower() if videoselection != "2": @@ -44,8 +100,9 @@ else: playLocalFile = str(addon.getSetting("playLocalFile")).lower() filesinlists = int(addon.getSetting("filesinlists")) useYTDL = addon.getSetting("useytdl") +useInvidious = addon.getSetting("useinvidious") # show if video is unofficial in title -showunoffintitle = addon.getSetting("showunoffintitle") +showunoffintitle = addon.getSetting("showunoffintitle") # summed up provider playcount considered to be played often pcounthigh = int(addon.getSetting("pcounthigh")) # local playcount considered to be played often @@ -58,8 +115,7 @@ ccounthigh = int(addon.getSetting("ccounthigh")) likecounthigh = int(addon.getSetting("likecounthigh")) # dislike count considered to be high dislikecounthigh = int(addon.getSetting("dislikecounthigh")) -# needed for comapatibilty mode used with KODI <= 17 aka Krypton -xbmcversion = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) + # get folder settings showincoming = addon.getSetting("show-incoming") showincominghits = addon.getSetting("show-incoming-hits") @@ -71,14 +127,23 @@ showcountries = addon.getSetting("show-countries") showsearch = addon.getSetting("show-search") showmuslyrandom = addon.getSetting("show-musly-random") showupdate = addon.getSetting("show-update") +showsettings = addon.getSetting("show-settings") +shownotifications = addon.getSetting("show-notifications") + +preferedprovider = addon.getSetting("prefered-provider").lower() # play from here. Does work in general but refreshes the container which is bit contra-productive in random lists ;) # But it's ok in a musly-list?: -# XBMC.PlayMedia(plugin://plugin.video.vidfltr/?mode=sortTitlesBy&url=%26start%3d1555257469%26limit%3drelated%26sort%3dnone,isdir +# works: kodi-send --action='XBMC.PlayMedia(plugin://plugin.video.vidfltr/?mode=sortTitlesBy&url=&start=1555257469&limit=related&sort=none,isdir)' if not os.path.isdir(addon_work_folder): os.mkdir(addon_work_folder) + +def log(msg, level=xbmc.LOGINFO): + xbmc.log("[%s %s] %s" % (addon_name, addon_version, msg), level=level) + + def getVideos(): if not os.path.isfile(jsonVideos): updateData() @@ -87,10 +152,11 @@ def getVideos(): now = time.time() if now - fileTime > maxFileAge: updateData() - with io.open(jsonVideos, 'r', encoding='utf-8') as f: + with gzip.open(jsonVideos, "r") as f: data = json.load(f) return data + def getArtists(): if not os.path.isfile(jsonArtists): updateData() @@ -99,10 +165,11 @@ def getArtists(): now = time.time() if now - fileTime > maxFileAge: updateData() - with io.open(jsonArtists, 'r', encoding='utf-8') as f: + with gzip.open(jsonArtists, "r") as f: data = json.load(f) return data + def getStyles(): if not os.path.isfile(jsonStyles): updateData() @@ -111,372 +178,800 @@ def getStyles(): now = time.time() if now - fileTime > maxFileAge: updateData() - with io.open(jsonStyles, 'r', encoding='utf-8') as f: + with gzip.open(jsonStyles, "r") as f: data = json.load(f) return data + def updateData(): - try: - target = urllib.URLopener() - target.retrieve("https://vidfltr.slashproc.org/api/videos.json", jsonVideos) - target = urllib.URLopener() - target.retrieve("https://vidfltr.slashproc.org/api/artists.json", jsonArtists) - target = urllib.URLopener() - target.retrieve("https://vidfltr.slashproc.org/api/styles.json", jsonStyles) + try: + target.retrieve( + "https://vidfltr.slashproc.org/api/artists.json.gz", jsonArtists + ) + target.retrieve("https://vidfltr.slashproc.org/api/styles.json.gz", jsonStyles) + target.retrieve("https://vidfltr.slashproc.org/api/videos.json.gz", jsonVideos) + if shownotifications == "true": + xbmcgui.Dialog().notification("", translation(30140), icon, 250) except: - # return to getVideos/getArtists on network error or 404 or... + if shownotifications == "true": + xbmcgui.Dialog().notification( + "", translation(30141), xbmcgui.NOTIFICATION_ERROR, 1000 + ) + # return to getVideos/getArtists on network error or 404 or... # ...to make it not fatal if the json can't be updated - return + log(msg="could not get data", level=xbmc.LOGERROR) + return + def alphabet(): - alphabet = ['#'] + alphabet = ["#"] for letter in range(65, 91): alphabet.append(chr(letter)) return alphabet -def main(): - if showincoming == "true": - addDir(translation(30007), '&limit=all&sort=date&start=0', 'sortTitlesBy', fanart) - if showincominghits == "true": - addDir(translation(30008), '&limit=all&sort=date&start=0&hit=true', 'sortTitlesBy', fanart) - if showrandom == "true": - addDir(translation(30029), '', 'mainrandom', fanart) - if showsorted == "true": - addDir(translation(30012), '', 'mainsorted', fanart) - if showstyles == "true": - addDir(translation(30022), '&sort=all', 'styles', fanart) - if showartists == "true": - addDir(translation(30005), '', 'artists', fanart) - if showcountries == "true": - addDir(translation(30118), '', 'countrycodes', fanart) - if showsearch == "true": - addDir(translation(30001), '', 'search', fanart) - if showupdate == "true": - addDir(translation(30006), '', 'updateData', fanart) - endOfDirectory() - def artists(): for alpha in alphabet(): - addDir(alpha, '&style=artists&limit=' + alpha + '&start=0', 'showArtist', fanart) + addDir( + alpha, "&limit=" + alpha + "&style=artists&start=0", "showArtist", fanart + ) endOfDirectory() + def countrycodes(): data = getArtists() result = [] for entry in data: - countrycode = entry['countrycode'] + countrycode = entry["countrycode"] if countrycode != "" and countrycode not in result: result.append(countrycode) elif countrycode == "" and translation(30124) not in result: result.append(translation(30124)) result.sort() for entry in result: - addDir(entry, '&style=country&limit=' + entry + '&start=0', 'showArtist', fanart, len(result)) + addDir( + entry, + "&limit=" + entry + "&style=country&start=0", + "showArtist", + fanart, + len(result), + ) endOfDirectory() + +def main(): + if showincoming == "true": + addDir( + translation(30007), "&limit=all&sort=date&start=0", "sortTitlesBy", fanart + ) + if showincominghits == "true": + addDir( + translation(30008), + "&limit=all&sort=date&start=0&hit=true", + "sortTitlesBy", + fanart, + ) + if showrandom == "true": + addDir(translation(30029), "", "mainrandom", fanart) + if showsorted == "true": + addDir(translation(30012), "", "mainsorted", fanart) + if showstyles == "true": + addDir(translation(30022), "&sort=all", "styles", fanart) + if showartists == "true": + addDir(translation(30005), "", "artists", fanart) + if showcountries == "true": + addDir(translation(30118), "", "countrycodes", fanart) + if showsearch == "true": + addDir(translation(30001), "", "search", fanart) + if showupdate == "true": + addDir(translation(30006), "", "updateData", fanart) + if showsettings == "true": + addDir(translation(30009), "", "opensettings", fanart) + endOfDirectory() + + def mainrandom(): - addDir(translation(30034), '&limit=year&sort=random&start=0', 'sortTitlesBy', fanart) - addDir(translation(30035), '&limit=year&sort=random&start=0&hit=true', 'sortTitlesBy', fanart) - addDir(translation(30017), '&limit=all&sort=random&start=0', 'sortTitlesBy', fanart) - addDir(translation(30031), '&limit=all&sort=random&start=0&hit=true', 'sortTitlesBy', fanart) - addDir(translation(30117), '&sort=random', 'numbers', fanart) - addDir(translation(30022), '&sort=random', 'styles', fanart) if showmuslyrandom == "true": - addDir(translation(30024), '&limit=related&start=0&sort=none', 'sortTitlesBy', fanart) + addDir( + translation(30024), + "&limit=related&start=0&sort=none", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30035), "&limit=year&sort=random&start=0", "sortTitlesBy", fanart + ) + addDir( + translation(30034), + "&limit=year&sort=random&start=0&hit=true", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30149), "&limit=years&sort=random&start=0", "sortTitlesBy", fanart + ) + addDir( + translation(30148), + "&limit=years&sort=random&start=0&hit=true", + "sortTitlesBy", + fanart, + ) + + addDir(translation(30017), "&limit=all&sort=random&start=0", "sortTitlesBy", fanart) + addDir( + translation(30031), + "&limit=all&sort=random&start=0&hit=true", + "sortTitlesBy", + fanart, + ) + addDir(translation(30022), "&sort=random", "styles", fanart) + #addDir( + # translation(30147), + # "&limit=color&sort=random&start=#FEFEFE", + # "sortTitlesBy", + # fanart, + #) + # addDir(translation(30117), '&sort=random', 'numbers', fanart) + addDir( + translation(30115), + "&limit=all&sort=random&start=0&count=likes", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30116), + "&limit=all&sort=random&start=0&count=dislikes", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30119), + "&limit=all&sort=random&start=0&count=controversial", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30114), + "&limit=all&sort=random&start=0&count=comments", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30111), + "&limit=all&sort=random&start=0&count=pcount", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30113), + "&limit=all&sort=random&start=0&count=acount", + "sortTitlesBy", + fanart, + ) + addDir(translation(30174), "&limit=camelot&sort=random", "camelot", fanart) endOfDirectory() + def mainsorted(): - addDir(translation(30032), '&limit=all&sort=date&start=0', 'sortTitlesBy', fanart) - addDir(translation(30033), '&limit=all&sort=date&start=0&hit=true', 'sortTitlesBy', fanart) - addDir(translation(30117), '&sort=sorted', 'numbers', fanart) - addDir(translation(30022), '&sort=sorted', 'styles', fanart) + addDir(translation(30032), "&limit=all&sort=date&start=0", "sortTitlesBy", fanart) + addDir( + translation(30033), + "&limit=all&sort=date&start=0&hit=true", + "sortTitlesBy", + fanart, + ) + # addDir(translation(30117), '&sort=sorted', 'numbers', fanart) + addDir( + translation(30111), + "&limit=all&sort=count&start=0&count=pcount", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30113), + "&limit=all&sort=count&start=0&count=acount", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30114), + "&limit=all&sort=count&start=0&count=comments", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30115), + "&limit=all&sort=count&start=0&count=likes", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30116), + "&limit=all&sort=count&start=0&count=dislikes", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30119), + "&limit=all&sort=count&start=0&count=controversial", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30142), + "&limit=all&sort=count&start=0&count=duration", + "sortTitlesBy", + fanart, + ) + addDir(translation(30174), "&limit=camelot&sort=date", "camelot", fanart) endOfDirectory() + def numbers(): if sort == "random": - addDir(translation(30111), '&limit=all&sort=random&start=0&count=pcount', 'sortTitlesBy', fanart) - addDir(translation(30113), '&limit=all&sort=random&start=0&count=acount', 'sortTitlesBy', fanart) - addDir(translation(30114), '&limit=all&sort=random&start=0&count=comments', 'sortTitlesBy', fanart) - addDir(translation(30115), '&limit=all&sort=random&start=0&count=likes', 'sortTitlesBy', fanart) - addDir(translation(30116), '&limit=all&sort=random&start=0&count=dislikes', 'sortTitlesBy', fanart) - addDir(translation(30119), '&limit=all&sort=random&start=0&count=controversial', 'sortTitlesBy', fanart) + addDir( + translation(30111), + "&limit=all&sort=random&start=0&count=pcount", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30113), + "&limit=all&sort=random&start=0&count=acount", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30114), + "&limit=all&sort=random&start=0&count=comments", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30115), + "&limit=all&sort=random&start=0&count=likes", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30116), + "&limit=all&sort=random&start=0&count=dislikes", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30119), + "&limit=all&sort=random&start=0&count=controversial", + "sortTitlesBy", + fanart, + ) endOfDirectory() if sort == "sorted": - addDir(translation(30111), '&limit=all&sort=count&start=0&count=pcount', 'sortTitlesBy', fanart) - addDir(translation(30113), '&limit=all&sort=count&start=0&count=acount', 'sortTitlesBy', fanart) - addDir(translation(30114), '&limit=all&sort=count&start=0&count=comments', 'sortTitlesBy', fanart) - addDir(translation(30115), '&limit=all&sort=count&start=0&count=likes', 'sortTitlesBy', fanart) - addDir(translation(30116), '&limit=all&sort=count&start=0&count=dislikes', 'sortTitlesBy', fanart) - addDir(translation(30119), '&limit=all&sort=count&start=0&count=controversial', 'sortTitlesBy', fanart) + addDir( + translation(30111), + "&limit=all&sort=count&start=0&count=pcount", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30113), + "&limit=all&sort=count&start=0&count=acount", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30114), + "&limit=all&sort=count&start=0&count=comments", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30115), + "&limit=all&sort=count&start=0&count=likes", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30116), + "&limit=all&sort=count&start=0&count=dislikes", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30119), + "&limit=all&sort=count&start=0&count=controversial", + "sortTitlesBy", + fanart, + ) + addDir( + translation(30142), + "&limit=all&sort=count&start=0&count=duration", + "sortTitlesBy", + fanart, + ) endOfDirectory() +def camelot(): + #fanart = "resources/img/camelot/camleot.png" + if sort == "random" or sort == "date": + num1 = 0 + for num in range(1, 13): + # logic to get get all the different translation strings from camelot wheel + minor = 30149 + num + num1 + major = 30149 + num + num1 + 1 + log( + msg="camelotnumber=%s, minor=%s, major=%s" % (num, minor, major), + level=xbmc.LOGINFO, + ) + fanart = os.path.join( + addonDir, "resources/img/camelot/" + str(num) + "A.png" + ) + addDir( + translation(minor), + "&limit=camelot&sort=" + sort + "&start=" + str(num) + "A", + "sortTitlesBy", + fanart, + ) + fanart = os.path.join( + addonDir, "resources/img/camelot/" + str(num) + "B.png" + ) + addDir( + translation(major), + "&limit=camelot&sort=" + sort + "&start=" + str(num) + "B", + "sortTitlesBy", + fanart, + ) + num1 += 1 + endOfDirectory() + + +def opensettings(): + try: + xbmcaddon.Addon().openSettings() + except: + return + + +def openytdlsettings(): + try: + xbmcaddon.Addon("script.module.youtube.dl").openSettings() + except: + return + + try: + xbmcaddon.Addon().openSettings() + execute("SetFocus(addon-youtube)") + except: + return + + def play(url): data = getVideos() result = [] slug = url if slug != "": for entry in data: - if entry['slug'] == slug: + if entry["slug"] == slug: + playcount = int(entry["acount"]) # play from local file. # does only work on addon developer pc -- atleast for now if playLocalFile == "true": musicvideoid = "-1" + log(msg="trying to play from local file", level=xbmc.LOGDEBUG) try: - # try to find music video in library with utc time from slug - dateadded = datetime.utcfromtimestamp(float(entry['slug'])) - dateadded = str(dateadded) - jsonRespond = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "params": {"sort": {"order": "ascending", "method": "title"}, "filter": {"operator": "contains", "field": "dateadded", "value": "%s"}, "properties": ["file", "playcount", "genre", "artist", "title"]}, "limits":{"end":0,"start":0}, "method": "VideoLibrary.GetMusicvideos", "id": "libMusicVideos"}' % dateadded) - local = json.loads(jsonRespond) - for vid in local["result"]["musicvideos"]: + # try to find music video in library with localtime converted from utc time from slug + dateadded = strftime( + "%Y-%m-%d %H:%M:%S", localtime(float(entry["slug"])) + ) + dateadded = str(dateadded) + jsonRespond = xbmc.executeJSONRPC( + '{"jsonrpc": "2.0", "params": {"sort": {"order": "ascending", "method": "title"}, "filter": {"operator": "contains", "field": "dateadded", "value": "%s"}, "properties": ["file", "playcount", "genre", "artist", "title"]}, "limits":{"end":0,"start":0}, "method": "VideoLibrary.GetMusicvideos", "id": "libMusicVideos"}' + % dateadded + ) + response = json.loads(jsonRespond) + for vid in response["result"]["musicvideos"]: + playcount = vid["playcount"] playback_url = vid["file"] musicvideoid = int(vid["musicvideoid"]) - playcount = int(vid['playcount']) - genre = vid['genre'] - artist = vid['artist'] - artist = ''.join(artist) - title = vid['title'] - title = (artist + ' - ' + title ) - xbmc.log(msg="date from slug", level=xbmc.LOGDEBUG) - + # playcount = int(vid['playcount']) + genre = vid["genre"] + artist = vid["artist"] + artist = "".join(artist) + title = vid["title"] + title = artist + " - " + title + log( + msg="found in db: musicvideoid: %s, playcount=%s, url=%s" + % ( + str(musicvideoid), + str(playcount), + str(playback_url), + ), + level=xbmc.LOGINFO, + ) except: - try: - # if not found because of inconsistencies based on MEZ vs. MESZ try to find music video in library with time from dateadded - dateadded = entry['dateadded'] - jsonRespond = xbmc.executeJSONRPC('{"jsonrpc": "2.0", "params": {"sort": {"order": "ascending", "method": "title"}, "filter": {"operator": "contains", "field": "dateadded", "value": "%s"}, "properties": ["file", "playcount", "genre", "artist", "title"]}, "limits":{"end":0,"start":0}, "method": "VideoLibrary.GetMusicvideos", "id": "libMusicVideos"}' % dateadded) - local = json.loads(jsonRespond) - for vid in local["result"]["musicvideos"]: - playback_url = vid["file"] - musicvideoid = int(vid["musicvideoid"]) - playcount = int(vid['playcount']) - genre = vid['genre'] - artist = vid['artist'] - artist = ''.join(artist) - title = vid['title'] - title = (artist + ' - ' + title ) - xbmc.log(msg="date from dateadded", level=xbmc.LOGDEBUG) - except: - if useYTDL == "true": - # if play from local file is set but nonetheless not found use Provider-URL - xbmc.log(msg="not found, play from provider", level=xbmc.LOGDEBUG) - import YDStreamExtractor - idVideo = entry['purl'] - ytdl = YDStreamExtractor.getVideoInfo(idVideo) - playback_url = ytdl.streamURL() - musicvideoid = "-1" - else: - # without youtube_dl - xbmc.log(msg="without ytdl", level=xbmc.LOGDEBUG) - playback_url = entry['url'] - - # default mode: play from provider -- either with ytdl or vimeo/youtube/dailymotion addon + # if play from local file is set but nonetheless not found use Provider-URL + playback_url = resolveprovider(entry) + log( + msg="could not find musicvideoid, play from provider: url= %s" + % playback_url, + level=xbmc.LOGERROR, + ) else: - xbmc.log(msg="play from provider", level=xbmc.LOGDEBUG) - if useYTDL == "true": - xbmc.log(msg="with ytdl", level=xbmc.LOGDEBUG) - import YDStreamExtractor - idVideo = entry['purl'] - ytdl = YDStreamExtractor.getVideoInfo(idVideo) - playback_url = ytdl.streamURL() - else: - if "vimeo" in entry['provider']: - if xbmc.getCondVisibility("System.HasAddon(plugin.video.vimeo)") and int((xbmcaddon.Addon("plugin.video.vimeo").getAddonInfo("version").strip().replace(".",""))) >= 500: - xbmc.log(msg="using the new Vimeo addon from jaylinski", level=xbmc.LOGNOTICE) - playback_url = entry['url'] - else: - # quick hard coded workaround - xbmc.log(msg="old vimeo addon is broken, playing with ytdl. Please install the new jaylinski Vimeo Addon from the official KODI Repo", level=xbmc.LOGNOTICE) - import YDStreamExtractor - idVideo = entry['purl'] - ytdl = YDStreamExtractor.getVideoInfo(idVideo) - playback_url = ytdl.streamURL() - else: - # without youtube_dl - xbmc.log(msg="without ytdl", level=xbmc.LOGDEBUG) - playback_url = entry['url'] + log(msg="play from provider", level=xbmc.LOGDEBUG) + playback_url = resolveprovider(entry) item = xbmcgui.ListItem(path=playback_url) - # avoids CCurlFile::Stat - Failed: Unsupported protocol(1) for plugin:// - # disabled, unclear if this is the way to - #item.setContentLookup(False) + # disabled, unclear if this is the way to go + # item.setContentLookup(False) # add some Listitem info for local files # should already be set in addVideo() but it isn't, doh! if playLocalFile == "true" and not musicvideoid == "-1": - item.setInfo(type="video", infoLabels={"mediatype": mediatype, "dbid": musicvideoid, "title": title, "genre": genre }) - -# doesn't work -# item.setProperty('StartOffset', '56.4') -# Bug? It maybe works with StartPercent, see https://forum.kodi.tv/showthread.php?tid=129188&pid=2443114#pid2443114 -# item.setProperty("VolumeAmplification", "7") -# xbmc.log(msg=item.getProperty('VolumeAmplification'), level=xbmc.LOGNOTICE) + item.setPath(path=playback_url) + # deprecated, use https://xbmc.github.io/docs.kodi.tv/master/kodi-dev-kit/group__python___info_tag_video.html + # https://github.com/xbmc/xbmc/pull/19459#issuecomment-806450382 + mediatype = addon.getSetting("mediatype") + item.setInfo( + type="video", + infoLabels={ + "mediatype": mediatype, + "dbid": musicvideoid, + "playcount": playcount, + "title": title, + "genre": genre, + }, + ) + log( + msg="from local file musicvideoid=%s, playback_url=%s" + % (str(musicvideoid), str(playback_url)), + level=xbmc.LOGINFO, + ) + # nfo is the ultimate source for local playcount + try: + import xbmcvfs + import xml.etree.ElementTree as ElTr - xbmc.log(msg=playback_url, level=xbmc.LOGNOTICE) - xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, item) + nfo = "%s.nfo" % os.path.splitext(str(playback_url))[0] + with xbmcvfs.File(nfo, "r") as f: + xml = ElTr.ElementTree(ElTr.fromstring(f.read())) + root = xml.getroot() + + # looking for tag 'playcount' create it if necessary and set content with playcount + xml_playcount = ( + ElTr.SubElement(root, "playcount") + if root.find("playcount") is None + else root.find("playcount") + ) + playcount = xml_playcount.text + except: + log( + msg="could not get playcount from nfo for %s" % str(nfo), + level=xbmc.LOGERROR, + ) + playcount = int(entry["acount"]) + # write playcount into db if it does not match playcount from nfo + if int(response["result"]["musicvideos"][0]["playcount"]) != int(playcount): + log( + msg="setting playcount from nfo: id=%s, playcountnfo=%s, playcountdb=%s" + % ( + str(musicvideoid), + str(playcount), + str(response["result"]["musicvideos"][0]["playcount"]), + ), + level=xbmc.LOGINFO, + ) + try: + setplaycount = xbmc.executeJSONRPC( + '{"jsonrpc":"2.0","method":"VideoLibrary.SetMusicVideoDetails","params":{"musicvideoid": %s, "playcount": %s}, "id":1}' + % (str(musicvideoid), str(playcount)) + ) + # setplaycount = xbmc.executeJSONRPC('{"jsonrpc":"2.0","method":"VideoLibrary.SetMusicVideoDetails","params":{"musicvideoid": %s, "playcount": %s}}' % (str(musicvideoid), str(playcount))) + # log(msg="current db playcount from response: %s" % str(response["result"]["musicvideos"]["playcount"]), level=xbmc.LOGINFO) + log( + msg="current db playcount response: %s" + % str(json.loads(setplaycount)), + level=xbmc.LOGDEBUG, + ) + except: + error = json.loads(setplaycount) + log( + msg="set playcount from nfo failed: %s" % str(error), + level=xbmc.LOGERROR, + ) + # item.setProperty('IsPlayable', 'true') + # xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, item) + # player = xbmc.Player().play(item=playback_url, listitem=item) + # return player + # xbmc.sleep(1000) + # while player.isPlaying(): + # xbmc.sleep(1000) + # log(msg="playback stopped", level=xbmc.LOGERROR) + # log(msg="playback stopped", level=xbmc.LOGERROR) + + # doesn't work + # item.setProperty('StartOffset', '56.4') + # Bug? It maybe works with StartPercent, see https://forum.kodi.tv/showthread.php?tid=129188&pid=2443114#pid2443114 + # item.setProperty("VolumeAmplification", "7") + # log(msg=item.getProperty('VolumeAmplification'), level=xbmc.LOGWARNING) + + # else: + item.setProperty("IsPlayable", "true") + log(msg="Playing %s" % playback_url, level=xbmc.LOGINFO) + ok = xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, item) + # monitor playback only if we play from local file + if playLocalFile == "true" and not musicvideoid == "-1": + xbmc.sleep(500) # Wait until playback starts + while xbmc.Player().isPlaying(): + xbmc.sleep(1000) + try: + # if playback ended count playcount 1 up and write to db. + # afterwards "Watchedstate NFO Updater" hopefully updates the nfo + playcount = int(playcount) + 1 + playcountafterplay = xbmc.executeJSONRPC( + '{"jsonrpc":"2.0","method":"VideoLibrary.SetMusicVideoDetails","params":{"musicvideoid": %s, "playcount": %s}, "id":1}' + % (str(musicvideoid), str(playcount)) + ) + success = json.loads(playcountafterplay) + log( + msg="set playcount=%s in db after play for %s: %s" + % (str(playcount), playback_url, str(success)), + level=xbmc.LOGINFO, + ) + except: + error = json.loads(playcountafterplay) + log( + msg="set playcount in db failed error=%s, url=" + % (str(error), playback_url), + level=xbmc.LOGERROR, + ) + return ok + + +def resolveprovider(entry): + log(msg="resolving provider", level=xbmc.LOGDEBUG) + if len(entry["sources"]) > 1: + if ( + preferedprovider == "none" + or entry["sources"][0]["provider"] == preferedprovider + or int(entry["sources"][0]["width"]) > int(entry["sources"][1]["width"]) + ): + provider = str(entry["sources"][0]["provider"]) + videoid = str(entry["sources"][0]["videoid"]) + restricted = int(entry["sources"][0]["age_limit"]) + elif entry["sources"][1]["provider"] == preferedprovider: + provider = str(entry["sources"][1]["provider"]) + videoid = str(entry["sources"][1]["videoid"]) + restricted = int(entry["sources"][1]["age_limit"]) + else: + provider = str(entry["sources"][0]["provider"]) + videoid = str(entry["sources"][0]["videoid"]) + restricted = int(entry["sources"][0]["age_limit"]) + else: + provider = str(entry["sources"][0]["provider"]) + videoid = str(entry["sources"][0]["videoid"]) + agerestricted = 0 + agerestricted = int(entry["sources"][0]["age_limit"]) + slug = str(entry["slug"]) + addonyoutube = addon.getSetting("addon-youtube") + addonyoutubeagerestricted = addon.getSetting("addon-youtube-age-restricted") + addonvimeo = addon.getSetting("addon-vimeo") + addondailymotion = addon.getSetting("addon-dailymotion") + addonfacebook = addon.getSetting("addon-facebook") + addonvevo = addon.getSetting("addon-vevo") + + if provider == "youtube": + if agerestricted >= 18: + addonyoutube = "script.module.youtube.dl" + playback_url = "https://www.youtube.com/watch?v=%s" % (videoid) + elif addonyoutube == "plugin.video.youtube": + playback_url = "plugin://plugin.video.youtube/play/?video_id=%s" % (videoid) + elif addonyoutube == "plugin.video.invidious": + try: + # wget --user=kodi --password=kodi -q -O- 'http://tanix:8080/jsonrpc?request={"jsonrpc": "2.0", "id": 1, "method": "Addons.GetAddonDetails", "params": {"addonid": "plugin.video.invidious", "properties": ["author"]}}' | jq '.result .addon .author' + jsonRespond = xbmc.executeJSONRPC( + '{"jsonrpc": "2.0", "id": 1, "method": "Addons.GetAddonDetails", "params": {"addonid": "plugin.video.invidious", "properties": ["author"]}}' + ) + author = json.loads(jsonRespond)["result"]["addon"]["author"] + log(msg="plugin.video.invidious author: %s" % str(author),level=xbmc.LOGINFO) + if author == "petterreinholdtsen" or author == "TheAssassin": + # provider-name="TheAssassin" or provider-name="petterreinholdtsen"> + playback_url = "plugin://plugin.video.invidious/?action=play_video&video_id=%s" % (videoid) + else: + # provider-name="lekma" + # playback_url = ("plugin://plugin.video.invidious/?action=video&videoId=%s" % (videoid)) + playback_url = "plugin://plugin.video.invidious/?action=play&yt=true&videoId=%s" % (videoid) + except: + log(msg="no variant of plugin.video.invidious found, trying script.module.youtube.dl", level=xbmc.LOGERROR) + addonyoutube == "script.module.youtube.dl" + playback_url = "https://www.youtube.com/watch?v=%s" % (videoid) + elif addonyoutube == "plugin.video.tubed": + playback_url = "plugin://plugin.video.tubed/?mode=play&video_id=%s" % (videoid) + elif addonyoutube == "script.module.youtube.dl": + playback_url = "https://www.youtube.com/watch?v=%s" % (videoid) + elif addonyoutube == "plugin.video.sendtokodi": + playback_url = "plugin://plugin.video.sendtokodi/?https://www.youtube.com/watch?v=%s" % (videoid) + provider = addonyoutube + elif provider == "vimeo": + if addonvimeo == "plugin.video.vimeo": + playback_url = "plugin://plugin.video.vimeo/play/?video_id=%s" % (videoid) + elif addonvimeo == "script.module.youtube.dl": + playback_url = "https://vimeo.com/%s" % (videoid) + elif addonvimeo == "plugin.video.sendtokodi": + playback_url = "plugin://plugin.video.sendtokodi/?https://vimeo.com/%s" % ( + videoid + ) + provider = addonvimeo + elif provider == "dailymotion": + if addondailymotion == "plugin.video.dailymotion_com": + playback_url = ( + "plugin://plugin.video.dailymotion_com/?mode=playVideo&url=%s" + % (videoid) + ) + elif addondailymotion == "script.module.youtube.dl": + playback_url = "https://www.dailymotion.com/video/%s" % (videoid) + elif addondailymotion == "plugin.video.sendtokodi": + playback_url = ( + "plugin://plugin.video.sendtokodi/?https://www.dailymotion.com/video/%s" + % (videoid) + ) + provider = addondailymotion + elif provider == "facebook": + if addonfacebook == "script.module.youtube.dl": + playback_url = "https://www.facebook.com/video.php?v=%s" % (videoid) + elif addonfacebook == "plugin.video.sendtokodi": + playback_url = ( + "plugin://plugin.video.sendtokodi/?https://www.facebook.com/video.php?v=%s" + % (videoid) + ) + provider = addonfacebook + elif provider == "vevo": + if addonvevo == "script.module.youtube.dl": + playback_url = "https://www.vevo.com/watch/%s" % (videoid) + elif addonvevo == "plugin.video.sendtokodi": + playback_url = ( + "plugin://plugin.video.sendtokodi/?https://www.vevo.com/watch/%s" + % (videoid) + ) + provider = addonvevo + if provider == "script.module.youtube.dl": + import YDStreamExtractor + + ytdl = YDStreamExtractor.getVideoInfo(playback_url) + playback_url = ytdl.streamURL() + return playback_url def styles(): - data = getVideos() + data = getStyles() channels = [] - xbmc.log(msg=sort, level=xbmc.LOGDEBUG) + log(msg=sort, level=xbmc.LOGDEBUG) for entry in data: - if entry['style'] not in channels: - channels.append(entry['style']) - length = len(channels) + if entry["style"] not in channels: + channels.append(entry["style"]) + length = len(channels) channels.sort() for channel in channels: -# don't use special folder icons as long as there is'nt a nice icon for every style -# addDir(channel, '&style=' + channel + '&sort=' + sort, 'showChannel', getFanart(channel), length) -# don't use special folder icons as long as there is'nt a nice icon for every style - addDir(channel, '&style=' + channel + '&sort=' + sort, 'showChannel', fanart, length) + fanart = getFanart(channel) + addDir(channel, "&style=" + channel + "&sort=" + sort, "showStyle", fanart) endOfDirectory() -def showChannel(style, sort): +def showStyle(style, sort): channel = style if channel != "": - fanart = getFanart(channel) - xbmc.log(msg=channel, level=xbmc.LOGDEBUG) + fanart = "" + log(msg=channel, level=xbmc.LOGDEBUG) if sort == "sorted" or sort == "all": - addDir(translation(30032), '&limit=' + channel + '&sort=date&start=0', 'sortTitlesBy', fanart, 6) - addDir(translation(30033), '&limit=' + channel + '&sort=date&start=0&hit=true', 'sortTitlesBy', fanart, 6) - # genre splitted artists, only menu entry which uses the old initials mode and - # therefore entries where the artist isn't at the beginning of the title are excluded :-( - addDir(translation(30005), channel, 'sortTitleInitials', fanart, 6) - if sort == "random" or sort == "all": - addDir(translation(30029), '&limit=' + channel + '&sort=random&start=0', 'sortTitlesBy', fanart, 6) - addDir(translation(30031), '&limit=' + channel + '&sort=random&start=0&hit=true', 'sortTitlesBy', fanart, 6) - endOfDirectory() + addDir( + translation(30032), + "&limit=" + channel + "&sort=date&start=0", + "sortTitlesBy", + fanart, + 6, + ) + addDir( + translation(30033), + "&limit=" + channel + "&sort=date&start=0&hit=true", + "sortTitlesBy", + fanart, + 6, + ) - -def sortTitleInitials(channel=""): - data = getVideos() - result = [] - fanart = getFanart(channel) - if channel != "": - for entry in data: - if entry['style'] == channel: - if len(entry['title']) > 0: - l = entry['title'][0].upper() - if not re.match('^([a-z|A-Z])', l): - l = '#' - if l not in result: - if videoselection == "0": - if "true" in entry['official'].lower(): - result.append(l) - elif videoselection == "1": - if "false" in entry['official'].lower(): - result.append(l) - else: - result.append(l) - result.sort() - for entry in result: - addDir(entry, channel + '|' + entry, 'sortTitles', fanart, len(result)) - endOfDirectory() - - -def sortArtistInitials(channel=""): - data = getVideos() - result = [] - fanart = getFanart(channel) - if channel != "": - for entry in data: - for artist in entry['artists']: -# xbmc.log(msg=channel.encode('utf-8'), level=xbmc.LOGNOTICE) -# xbmc.log(msg=artist.encode('utf-8'), level=xbmc.LOGNOTICE) - if artist not in channel: - if len(artist) > 0: - l = artist[0].upper() - if not re.match('^([a-z|A-Z])', l): - l = '#' - if l not in result: - if videoselection == "0": - if "true" in entry['official'].lower(): - result.append(l) - elif videoselection == "1": - if "false" in entry['official'].lower(): - result.append(l) - else: - result.append(l) - result.sort() - for entry in result: - addDir(entry, channel + '|' + entry, - 'sortArtists', fanart, len(result)) + if sort == "random" or sort == "all": + addDir( + translation(30029), + "&limit=" + channel + "&sort=random&start=0", + "sortTitlesBy", + fanart, + 6, + ) + addDir( + translation(30031), + "&limit=" + channel + "&sort=random&start=0&hit=true", + "sortTitlesBy", + fanart, + 6, + ) endOfDirectory() def sortTitles(channelInitial=""): - xbmcplugin.setContent(pluginhandle, 'musicvideos') + xbmcplugin.setContent(pluginhandle, "musicvideos") data = getVideos() result = [] params = channelInitial.split("|") - channel = "" + channel = limit initial = "" + log(msg=str(channel), level=xbmc.LOGDEBUG) if len(params) > 1: channel = params[0] initial = params[1] -# else: -# channel = params[0] + # else: + # channel = params[0] fanart = getFanart(channel) -# xbmc.log(msg=initial.encode('utf-8'), level=xbmc.LOGNOTICE) - if channel != "": - xbmc.log(msg=channel.encode('utf-8'), level=xbmc.LOGDEBUG) + if limit != "": + log(msg=channel.encode("utf-8"), level=xbmc.LOGDEBUG) for entry in data: - if entry['style'] == channel: - i = entry['title'][0].upper() - if initial == '#': - if not re.match('^([a-z|A-Z])', i): + if entry["style"] == channel: + i = entry["title"][0].upper() + if initial == "#": + if not re.match("^([a-z|A-Z])", i): if videoselection == "0": - if "true" in entry['official'].lower(): + if "true" in entry["official"].lower(): result.append(entry) elif videoselection == "1": - if "false" in entry['official'].lower(): + if "false" in entry["official"].lower(): result.append(entry) else: result.append(entry) else: if initial == i: if videoselection == "0": - if "true" in entry['official'].lower(): + if "true" in entry["official"].lower(): result.append(entry) elif videoselection == "1": - if "false" in entry['official'].lower(): + if "false" in entry["official"].lower(): result.append(entry) else: result.append(entry) + + log(msg=str(start), level=xbmc.LOGDEBUG) + # sloooow - #elif channel in entry['artists']: + # elif channel in entry['artists']: # result.append(entry) - result.sort(key=lambda entry: entry['title'].upper()) + result.sort(key=lambda entry: entry["title"].upper()) for entry in result: addVideo(entry) endOfDirectory() + def showArtist(style, limit, start): # limit artists by first letter or country data = getArtists() preresult = [] result = [] -# xbmc.log(msg=url, level=xbmc.LOGNOTICE) -# xbmc.log(msg=style, level=xbmc.LOGNOTICE) -# xbmc.log(msg=limit, level=xbmc.LOGNOTICE) + log(msg=url, level=xbmc.LOGDEBUG) + log(msg=style, level=xbmc.LOGDEBUG) + log(msg=limit, level=xbmc.LOGDEBUG) start = int(start) end = start + filesinlists nextstart = end + 1 - if style == 'artists': + if style == "artists": for entry in data: - i = unidecode(entry['artist'])[0].upper() - if limit == '#': - if not re.match('^([a-z|A-Z])', i): - preresult.append(entry) + i = unidecode(entry["artist"])[0].upper() + if limit == "#": + if not re.match("^([a-z|A-Z])", i): + preresult.append(entry) else: if limit == i: preresult.append(entry) - elif style == 'country': + + elif style == "country": for entry in data: - i = entry['countrycode'] + i = entry["countrycode"] if limit == i: preresult.append(entry) elif limit == translation(30124) and i == "": @@ -485,119 +980,156 @@ def showArtist(style, limit, start): # limit the result list according to the "Video Selection" Setting for entry in preresult: if videoselection == "0": - if entry['official'] >= "1": + if entry["official"] >= "1": result.append(entry) elif videoselection == "1": - if entry['unofficial'] >= "1": + if entry["unofficial"] >= "1": result.append(entry) elif videoselection == "2": - result.append(entry) + result.append(entry) maximum = len(result) for entry in result[start:end]: - artist = entry['artist'] - official = int(entry['official']) - unofficial = int(entry['unofficial']) + artist = entry["artist"] + artistslug = entry["slug"] + official = int(entry["official"]) + unofficial = int(entry["unofficial"]) - if entry['fanart'] != "": - fanart = entry['fanart'] - elif entry['thumbnail'] != "": - fanart = entry['thumbnail'] + if entry["fanart"] != "": + fanart = entry["fanart"] + elif entry["thumbnail"] != "": + fanart = entry["thumbnail"] else: fanart = "" if videoselection == "0": - addDir("%s (%s)" % (artist, official), unidecode(artist.upper()), 'sortArtists', fanart, official) + addDir( + "%s (%s)" % (artist, official), + "&limit=artist&start=" + artistslug, + "sortArtists", + fanart, + official, + ) elif videoselection == "1": - addDir("%s (%s)" % (artist, unofficial), unidecode(artist.upper()), 'sortArtists', fanart, unofficial) + addDir( + "%s (%s)" % (artist, unofficial), + "&limit=artist&start=" + artistslug, + "sortArtists", + fanart, + unofficial, + ) elif videoselection == "2": - addDir("%s (%s/%s)" % (artist, official, unofficial), unidecode(artist.upper()), 'sortArtists', fanart, (official + unofficial)) + addDir( + "%s (%s/%s)" % (artist, official, unofficial), + "&limit=artist&start=" + artistslug, + "sortArtists", + fanart, + (official + unofficial), + ) + if maximum > end: - pagemax = (float(maximum) / float(filesinlists)) - pagenext = (float(nextstart) / float(filesinlists)) - fanart = 'DefaultFolder.png' - addDir("%s: %s/%s" % (translation(30036), str(int(math.ceil(pagenext))), str(int(math.ceil(pagemax)))), '&style=' + style + '&limit=' + limit + '&start=' + str(nextstart), 'showArtist', fanart) + pagemax = float(maximum) / float(filesinlists) + pagenext = float(nextstart) / float(filesinlists) + fanart = "DefaultFolder.png" + addDir( + "%s: %s/%s" + % ( + translation(30036), + str(int(math.ceil(pagenext))), + str(int(math.ceil(pagemax))), + ), + "&limit=" + limit + "&style=" + style + "&start=" + str(nextstart), + "showArtist", + fanart, + ) endOfDirectory() -def sortArtists(channel=""): +def sortArtists(limit, start): # get videos for individual artist - xbmcplugin.setContent(pluginhandle, 'musicvideos') data = getVideos() result = [] - # using param=string parameters does not work here because some obscure chars - # are lost in translation so the old implementation from Mediathek Direkt with - # splitting on | will be used. Have to take a deeper look to fix this... - params = channel.split("|") -# xbmc.log(msg=url, level=xbmc.LOGNOTICE) - channel = params[0] - if len(params) > 1: - start = params[1] - #show "more from..." artists - if channel != "" and channel == "relartists": - fanart = getFanart(channel) - for entry in data: - if entry['slug'] == start: - artists_upper = [unidecode(artist.upper()) for artist in entry['artists']] - channels = [] - for channel in artists_upper: - channels.append(channel) + channel = limit - # nested loops don't look like an optimal solution and are a bit slow + if channel != "" and channel == "relartists": + artists = ast.literal_eval(urllib_parse.unquote_plus(start)) + # nested loops don't look like an optimal solution and are slow for entry in data: - for artist in channels: - if artist in [unidecode(name.upper()) for name in entry['artists']]: + artistslugs = entry["artistslugs"] + for artist in artists: + # slugartists = entry['artistslugs'] + if artist in artistslugs: if entry not in result: # limit selection based on videoselection setting if show all isn't activated if relatedselection != "true" and videoselection != "2": if videoselection == "0": - if "true" in entry['official'].lower(): + if "true" in entry["official"].lower(): result.append(entry) elif videoselection == "1": - if "false" in entry['official'].lower(): + if "false" in entry["official"].lower(): result.append(entry) else: result.append(entry) - # show artist - elif channel != "": - fanart = getFanart(channel) + # show artist + elif channel == "artist": + strictartists = [] + strictartists.append(start) + recurartists = [] for entry in data: - artists_upper = [unidecode(artist.upper()) for artist in entry['artists']] + slugartists = entry["artistslugs"] + if start in slugartists: + for artist in slugartists: + if artist not in recurartists: + recurartists.append(artist) + # does not work + # strictartists.sort() + recurartists.sort() + if strictartists != recurartists: + addDir( + translation(30144), + "&limit=relartists&start=" + quote_plus(str(strictartists)), + "sortArtists", + fanart, + len(result), + ) + addDir( + translation(30145), + "&limit=relartists&start=" + quote_plus(str(recurartists)), + "sortArtists", + fanart, + len(result), + ) + endOfDirectory() + else: + sortArtists("relartists", quote_plus(str(strictartists))) - if channel in artists_upper: - if videoselection == "0": - if "true" in entry['official'].lower(): - result.append(entry) - elif videoselection == "1": - if "false" in entry['official'].lower(): - result.append(entry) - else: - result.append(entry) - - result.sort(key=lambda entry: entry['title'].upper()) + result.sort(key=lambda entry: entry["title"].upper()) for entry in result: addVideo(entry) endOfDirectory() + def sortTitlesBy(limit, sort, start): - xbmcplugin.setContent(pluginhandle, 'musicvideos') + xbmcplugin.setContent(pluginhandle, "musicvideos") data = getVideos() result = [] channel = limit - start = int(start) - end = start + filesinlists - nextstart = end + 1 + if start.isdigit(): + start = int(start) + end = start + filesinlists + nextstart = end + 1 + fanart = getFanart(channel) # limit selection if "all" isn't activated in videoselection - #if videoselection != "2" and channel != "related" and channel != "relartists": + # if videoselection != "2" and channel != "related" and channel != "relartists": if videoselection != "2": data = limitselection(data) # limit to hits if hit != "" and hit == "true": for entry in data: - if entry['hit'] == "true": + if entry["hit"] == "true": result.append(entry) data = result result = [] @@ -606,7 +1138,7 @@ def sortTitlesBy(limit, sort, start): if mycount != "" and mycount == "pcount" and sort != "count": for entry in data: # played often at provider - if int(entry['pcount']) >= pcounthigh: + if int(entry["pcount"]) >= pcounthigh: result.append(entry) data = result result = [] @@ -614,15 +1146,15 @@ def sortTitlesBy(limit, sort, start): # played often by addon developer if mycount != "" and mycount == "acount" and sort != "count": for entry in data: - if int(entry['acount']) >= acounthigh: + if int(entry["acount"]) >= acounthigh: result.append(entry) data = result result = [] - + # often commented if mycount != "" and mycount == "comments" and sort != "count": for entry in data: - if int(entry['comments']) >= ccounthigh: + if int(entry["comments"]) >= ccounthigh: result.append(entry) data = result result = [] @@ -630,7 +1162,7 @@ def sortTitlesBy(limit, sort, start): # often liked if mycount != "" and mycount == "likes" and sort != "count": for entry in data: - if int(entry['likes']) >= likecounthigh: + if int(entry["likes"]) >= likecounthigh: result.append(entry) data = result result = [] @@ -638,7 +1170,7 @@ def sortTitlesBy(limit, sort, start): # often disliked if mycount != "" and mycount == "dislikes" and sort != "count": for entry in data: - if int(entry['dislikes']) >= dislikecounthigh: + if int(entry["dislikes"]) >= dislikecounthigh: result.append(entry) data = result result = [] @@ -646,70 +1178,128 @@ def sortTitlesBy(limit, sort, start): # controversial based on nearly the same number of likes and dislikes if mycount != "" and mycount == "controversial": for entry in data: - if int(entry['controversial']) == int(1): + if int(entry["controversial"]) == int(1): result.append(entry) data = result result = [] - # limit by style - if channel != "" and channel != "all" and channel != "year" and channel != "related": + # by duration + if mycount != "" and mycount == "duration" and sort != "count": for entry in data: - if entry['style'] == channel: + if int(entry["duration"]) > 0: result.append(entry) + data = result + result = [] + + # limit by hex color + if channel != "" and channel == "color": + for entry in data: + if entry["dominantcolor"] == start: + result.append(entry) + data = result + result = [] + + # limit by musical key, tonality + if channel != "" and channel == "camelot": + for entry in data: + if entry["keycamelot"] == start: + result.append(entry) + data = result + result = [] + start = 0 + # limit by pre-determined style + if ( + channel != "" + and channel != "all" + and channel != "year" + and channel != "years" + and channel != "related" + and channel != "color" + and channel != "camelot" + ): + for entry in data: + if entry["style"] == channel: + result.append(entry) + start = 0 # or limit to last year # hm, either style or year, not both? elif channel != "" and channel == "year": - lastyear = datetime.now() - timedelta(days=365) + startfrom = datetime.now() - timedelta(days=365) for entry in data: # ehrm, first version worked for many days but now fails? # related to https://bugs.python.org/issue27400 ? try: - dateadded = datetime.strptime(entry['dateadded'], '%Y-%m-%d %H:%M:%S') + dateadded = datetime.strptime(entry["dateadded"], "%Y-%m-%d %H:%M:%S") except TypeError: import time - dateadded = datetime.fromtimestamp(time.mktime(time.strptime(entry['dateadded'], '%Y-%m-%d %H:%M:%S'))) - if dateadded >= lastyear: + + dateadded = datetime.fromtimestamp( + time.mktime(time.strptime(entry["dateadded"], "%Y-%m-%d %H:%M:%S")) + ) + if dateadded >= startfrom: result.append(entry) + elif channel != "" and channel == "years": + startfrom = datetime.now() - timedelta(days=1095) + for entry in data: + # ehrm, first version worked for many days but now fails? + # related to https://bugs.python.org/issue27400 ? + try: + dateadded = datetime.strptime(entry["dateadded"], "%Y-%m-%d %H:%M:%S") + except TypeError: + import time + + dateadded = datetime.fromtimestamp( + time.mktime(time.strptime(entry["dateadded"], "%Y-%m-%d %H:%M:%S")) + ) + if dateadded >= startfrom: + result.append(entry) + # related tracks (generated with musly) elif channel != "" and channel == "related": # random musly list if start == 0: - # pick a random item from the already limited list (based on videoselection setting) - entry = random.choice(data) - start = str(entry['slug']) + # pick a random item from the already limited list (based on videoselection setting) + entry = random.choice(data) + start = str(entry["slug"]) + # log(msg=str(start), level=xbmc.LOGWARNING) + else: - start = str(start) + start = str(start) + for entry in data: + if entry["slug"] == start: + slugs = [] + # add slug for which the playlist should be shown + slugs.extend(entry["slug"].split(",")) + # and add all related slugs + slugs.extend(entry["related"].split(",")) + # now we have to add the unlimited video list # It will be limited later, again, if the videoselection # setting should also be honoured in related lists data = getVideos() - for entry in data: - if entry['slug'] == start: - slugs = [] - # add slug for which the playlist should be shown - slugs.extend(entry['slug'].split(", ")) - # and add all related slugs - slugs.extend(entry['related'].split(", ")) for entry in data: # add all related slugs - if entry['slug'] in slugs: + if entry["slug"] in slugs: # filter out tracks with the same title, where only the video differs. # if a related slug is listed earlier in data (= newer video) the # main slug will be filtered out. # In this case we add it again later, after sorting the result - if not filter(lambda result: result['title'] == entry['title'], result): - result.append(entry) + # if not filter(lambda result: result['title'] == entry['title'], result): + # log(msg=str(entry), level=xbmc.LOGWARNING) + result.append(entry) # slugs are sorted newest first because that's how they occur in the json # so we have to sort the result to the order in related aka the musly order (with prepended main slug) order_dict = {slug: index for index, slug in enumerate(slugs)} result.sort(key=lambda x: order_dict[x["slug"]]) + # log(msg=str(result), level=xbmc.LOGWARNING) + # check if the first entries slug is the main slug # if not remove it and add back the main entry. # I know, this is really ugly :-( - if result[0]['slug'] != start: + if result[0]["slug"] != start: result.pop(0) for entry in data: - if entry['slug'] == start: + if entry["slug"] == start: result.insert(0, entry) # limit selection? if videoselection != "2" and relatedselection != "true": @@ -719,101 +1309,148 @@ def sortTitlesBy(limit, sort, start): # related is a list with max 21 entries, so we have to set start always to 0 start = 0 - # "more from..." artists - elif channel != "" and channel == "relartists": - start = str(start) - for entry in data: - if entry['slug'] == start: - # add slug for which the artists should be shown - artists = "" - artists = entry['artists'] - for entry in data: - xbmc.log(msg=str(artists), level=xbmc.LOGNOTICE) - # add all entries with artist in artists - if entry['artists'] in artists: - result.append(entry) - xbmc.log(msg=str(results), level=xbmc.LOGNOTICE) + # "more from..." artists + # for entry in data: + # log(msg=str(artists), level=xbmc.LOGWARNING) + # add all entries with artist in artists + # if entry['artists'] in artists: + # result.append(entry) + # log(msg=str(results), level=xbmc.LOGWARNING) # limit selection? if videoselection != "2" and relatedselection != "true": result = limitselection(result) start = 0 - # sorted lists (by date and by numbers. But NOT controversial, which is a fixed list) + # last chance... else: for entry in data: result.append(entry) - if sort != "" and sort == "random": random.shuffle(result) result = result[:filesinlists] start = 0 + modus = "all" + # random is a fixed list, only one page of results end = filesinlists if sort != "" and sort == "date": if channel != "": modus = channel else: - modus = 'all' - result = result + modus = "all" if sort != "" and sort == "count": if channel != "": modus = channel else: - modus = 'all' + modus = "all" if mycount != "" and mycount == "controversial": # controversial is either 0 or 1 so it does not makes sense to sort upon it # instead sort videos considered controversial by dislikes # change controversial in videos.json to a float? - result = sorted(result, key = lambda i: (-1*float(i['dislikes']), i['title'])) + result = sorted( + result, key=lambda i: (-1 * float(i["dislikes"]), i["title"]) + ) + result.reverse() + elif mycount != "" and mycount == "duration": + # do a grouped result: shorter than 1 minute, 1-5 monutes, longer than 10 minutes? + result = sorted( + result, key=lambda i: (-1 * float(i["duration"]), i["title"]) + ) + # result.reverse() else: + # likes or dislikes # If count would'nt be negated one had to use reverse=true - # but then the second sorting, by title would be reversed also + # but then the second sorting, by title, would be reversed also # sort by negated count first and then by title in lexical order. - result = sorted(result, key = lambda i: (-1*float(i[mycount]), i['title'])) - # itemgetter, should be faster (hm, but does'nt work: ValueError: could not convert string to float: pcount) - # importing "operator" for implementing itemgetter - #from operator import itemgetter - #result = sorted(result, key=itemgetter((float(mycount), 'title'),reverse = True)) + result = sorted(result, key=lambda i: (-1 * float(i[mycount]), i["title"])) + # itemgetter, should be faster (hm, but does'nt work: ValueError: could not convert string to float: pcount) + # importing "operator" for implementing itemgetter + # from operator import itemgetter + # result = sorted(result, key=itemgetter((float(mycount), 'title'),reverse = True)) + + # play all (TODO) + # addDir(translation(30109), '', '', fanart) + # back to VIDFLTR (only useful if the back history would be cleared) + # if start > 0: + # addDir(translation(30108), '', 'main', fanart) + # hm, do I really want to replace the playlist on entering a folder? + # playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + # playlist.clear() + start = int(start) + end = start + filesinlists + nextstart = end + 1 -# play all (TODO) -# addDir(translation(30109), '', '', fanart) -# back to VIDFLTR (only useful if the back history would be cleared) -# if start > 0: -# addDir(translation(30108), '', 'main', fanart) -# hm, do I really want to replace the playlist on entering a folder? -# playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) -# playlist.clear() for entry in result[start:end]: addVideo(entry, mycount) maximum = len(result) if maximum > end and sort != "random" and channel != "year": - pagemax = (float(maximum) / float(filesinlists)) - pagenext = (float(nextstart) / float(filesinlists)) + pagemax = float(maximum) / float(filesinlists) + pagenext = float(nextstart) / float(filesinlists) # next page link if sort != "" and sort == "count": - addDir("%s: %s/%s" % (translation(30036), str(int(math.ceil(pagenext))), str(int(math.ceil(pagemax)))), '&limit=' + modus + '&sort=count&start=' + str(nextstart) + '&count=' + mycount, 'sortTitlesBy', fanart) + addDir( + "%s: %s/%s" + % ( + translation(30036), + str(int(math.ceil(pagenext))), + str(int(math.ceil(pagemax))), + ), + "&limit=" + + modus + + "&sort=count&start=" + + str(nextstart) + + "&count=" + + mycount, + "sortTitlesBy", + fanart, + ) elif hit != "" and hit == "true": - addDir("%s: %s/%s" % (translation(30036), str(int(math.ceil(pagenext))), str(int(math.ceil(pagemax)))), '&limit=' + modus + '&sort=date&start=' + str(nextstart) + '&hit=true', 'sortTitlesBy', fanart) + addDir( + "%s: %s/%s" + % ( + translation(30036), + str(int(math.ceil(pagenext))), + str(int(math.ceil(pagemax))), + ), + "&limit=" + modus + "&sort=date&start=" + str(nextstart) + "&hit=true", + "sortTitlesBy", + fanart, + ) else: - addDir("%s: %s/%s" % (translation(30036), str(int(math.ceil(pagenext))), str(int(math.ceil(pagemax)))), '&limit=' + modus + '&sort=date&start=' + str(nextstart), 'sortTitlesBy', fanart) + addDir( + "%s: %s/%s" + % ( + translation(30036), + str(int(math.ceil(pagenext))), + str(int(math.ceil(pagemax))), + ), + "&limit=" + modus + "&sort=date&start=" + str(nextstart), + "sortTitlesBy", + fanart, + ) endOfDirectory() def endOfDirectory(): -# xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_UNSORTED) -# xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_LABEL) -# xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_DATEADDED) -# xbmcplugin.addSortMethod( -# pluginhandle, xbmcplugin.SORT_METHOD_PROGRAM_COUNT) -# xbmcplugin.addSortMethod( -# pluginhandle, xbmcplugin.SORT_METHOD_VIDEO_RUNTIME) -# xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_GENRE) - xbmcplugin.endOfDirectory(pluginhandle) + # xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_NONE, label2Mask="%G") + # label2Mask="%G" -> https://github.com/xbmc/xbmc/blob/master/xbmc/filesystem/PluginDirectory.cpp + xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_NONE, label2Mask="%D") + # xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_PLAYLIST_ORDER, label2Mask="%V") + # xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_LABEL) + # xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_DATEADDED) + # xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_PROGRAM_COUNT) + # xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_VIDEO_RUNTIME) + # xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_GENRE) + # xbmcplugin.endOfDirectory(pluginhandle) + xbmcplugin.endOfDirectory( + pluginhandle, succeeded=True, updateListing=False, cacheToDisc=True + ) + def search(channel=""): - xbmcplugin.setContent(pluginhandle, 'musicvideos') + xbmcplugin.setContent(pluginhandle, "musicvideos") result = [] - keyboard = xbmc.Keyboard('', translation(30002)) + keyboard = xbmc.Keyboard("", translation(30002)) keyboard.doModal() if keyboard.isConfirmed() and keyboard.getText(): # search_string = keyboard.getText().encode('utf8').lower() @@ -822,13 +1459,13 @@ def search(channel=""): data = getVideos() for entry in data: cEntry = entry - if search_string in cEntry['title'].lower(): + if search_string in cEntry["title"].lower(): if channel != "": - if cEntry['style'] == channel: + if cEntry["style"] == channel: # cEntry['title'] = cEntry['style']+': # '+cEntry['title'] if videoselection == "0": - if "true" in entry['official'].lower(): + if "true" in entry["official"].lower(): result.append(cEntry) else: result.append(cEntry) @@ -836,17 +1473,17 @@ def search(channel=""): # cEntry['title'] = cEntry['style']+': # '+cEntry['title'] if videoselection == "0": - if "true" in entry['official'].lower(): + if "true" in entry["official"].lower(): result.append(cEntry) else: result.append(cEntry) - elif search_string in cEntry['artist'].lower(): + elif search_string in cEntry["artist"].lower(): if channel != "": - if cEntry['style'] == channel: + if cEntry["style"] == channel: # cEntry['title'] = cEntry['style']+': # '+cEntry['title'] if videoselection == "0": - if "true" in entry['official'].lower(): + if "true" in entry["official"].lower(): result.append(cEntry) else: result.append(cEntry) @@ -854,23 +1491,23 @@ def search(channel=""): # cEntry['title'] = cEntry['style']+': # '+cEntry['title'] if videoselection == "0": - if "true" in entry['official'].lower(): + if "true" in entry["official"].lower(): result.append(cEntry) else: result.append(cEntry) - - result.sort(key=lambda entry: entry['title'].lower()) + result.sort(key=lambda entry: entry["title"].lower()) for entry in result: addVideo(entry) endOfDirectory() + # not used (TODO?) def searchDate(channelDate=""): - xbmcplugin.setContent(pluginhandle, 'musicvideos') + xbmcplugin.setContent(pluginhandle, "musicvideos") channel = "" date = "" - params = channelDate.split('|') + params = channelDate.split("|") channel = params[0] if len(params) > 1: date = params[1] @@ -878,222 +1515,320 @@ def searchDate(channelDate=""): if date == "": dialog = xbmcgui.Dialog() date = dialog.numeric(1, translation(30007)) - date = re.sub('[^0-9|^\/]', '0', date) - date = date.replace('/', '.') + date = re.sub("[^0-9|^\/]", "0", date) + date = date.replace("/", ".") if (channel != "") and (len(date) == 10): data = getVideos() for entry in data: cEntry = entry - if (entry['style'] == channel) and (entry['date'] == date): - cEntry[1] = cEntry[2] + ': ' + cEntry[1] + if (entry["style"] == channel) and (entry["date"] == date): + cEntry[1] = cEntry[2] + ": " + cEntry[1] if videoselection == "0": - if "true" in entry['official'].lower(): + if "true" in entry["official"].lower(): result.append(cEntry) else: result.append(cEntry) - result.sort(key=lambda entry: entry['title'].lower()) + result.sort(key=lambda entry: entry["title"].lower()) for entry in result: addVideo(entry) endOfDirectory() + # not used (TODO?) def downloadFile(video_url): # get best qualiy url bq_url = getBestQuality(video_url) # get filname from video_url - filename = video_url.split('/')[-1] - filetype = filename.split('.')[-1] + filename = video_url.split("/")[-1] + filetype = filename.split(".")[-1] # open browser dialog to choose destination dialog = xbmcgui.Dialog() download_dir = dialog.browse(3, translation(30102), "files") target = urllib.URLopener() - fullPath = xbmc.translatePath(download_dir + filename) + fullPath = xbmcTranslatePath(download_dir + filename) target.retrieve(video_url, fullPath) dialog.ok(addonID, translation(30101), str(fullPath)) + # only used in style directories def getFanart(channel): - fanart = os.path.join( - addonDir, 'resources/images/fanart_' + channel + '.jpg') - if not os.path.isfile(fanart.encode('utf-8')): + fanart = os.path.join(addonDir, "resources/images/fanart_" + channel + ".jpg") + if not os.path.isfile(fanart.encode("utf-8")): fanart = icon return fanart + def limitselection(data): - xbmc.log(msg="limit by videoselection", level=xbmc.LOGDEBUG) + log(msg="limit by videoselection", level=xbmc.LOGDEBUG) result = [] for entry in data: - if videoselection == "0": - if "true" in entry['official']: + if entry not in result: + if videoselection == "0": + if "true" in entry["official"]: + result.append(entry) + elif videoselection == "1": + if "false" in entry["official"]: + result.append(entry) + elif videoselection == "2": result.append(entry) - elif videoselection == "1": - if "false" in entry['official']: - result.append(entry) - elif videoselection == "2": - result.append(entry) - return result + return result + def addDir(name, url, mode, iconimage, total=0): - u = sys.argv[0] + "?url=" + urllib.quote_plus(url.encode('utf-8')) + "&mode=" + str(mode) -# xbmc.log(msg=u, level=xbmc.LOGNOTICE) + # u = sys.argv[0] + "?url=" + str(url) + "&mode=" + str(mode) + u = sys.argv[0] + "?mode=" + str(mode) + str(url) + + log(msg=str(u), level=xbmc.LOGDEBUG) ok = True -# if (xbmcversion < 17): - liz = xbmcgui.ListItem(name, thumbnailImage=iconimage) -# else: -# With offscreen=true large lists (=folder) load much faster (needs >= krypton) -# But at the end, the differences are minimal in VIDFLTR, so just drop it :-) -# liz = xbmcgui.ListItem(name, iconImage=icon, thumbnailImage=iconimage, offscreen=True) + liz = xbmcgui.ListItem(name) + liz.setArt({"icon": iconimage}) description = "" - if mode == "showChannel": + if mode == "showStyle": data = getStyles() for style in data: - if style['style'] == name: - description = style['description'] + if style["style"] == name: + description = style["description"] - liz.setInfo(type="Video", infoLabels={"Title": name, "PlotOutline": description, "Plot": description, "Tagline": description}) + liz.setInfo( + type="Video", + infoLabels={ + "Title": name, + "PlotOutline": description, + "Plot": description, + "Tagline": description, + }, + ) if iconimage: - liz.setProperty("fanart_image", iconimage) + liz.setProperty("clearart", iconimage) else: - liz.setProperty("fanart_image", fanart) + liz.setProperty("clearart", fanart) if iconimage: - liz.setArt({ 'fanart' : iconimage }) + liz.setArt({"clearart": iconimage}) else: - liz.setArt({ 'fanart' : fanart }) - ok = xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=u, listitem=liz, totalItems=total, isFolder=True) + liz.setArt({"clearart": fanart}) + ok = xbmcplugin.addDirectoryItem( + handle=int(sys.argv[1]), url=u, listitem=liz, totalItems=total, isFolder=True + ) return ok def addVideo(entry, mycount="playcount"): - ok = True + # ok = True -# initialize the global video playlist. The item will be added later -# playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) + # initialize the global video playlist. The item will be added later + # playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO) # Altlast. Sinn dahinter? - if len(entry) > 7: - dateadded = datetime.utcfromtimestamp(float(entry['slug'])) - dateadded = str(dateadded) - slug = entry['slug'] - url = '%s?mode=play&url=%s' % (sys.argv[0], slug) - channel = entry['artist'] - artist = entry['artist'] - artists = entry['artists'] - director = entry['director'] - title = entry['title'] - title = title.replace(""", '"') - if "false" in entry['official'].lower() and "true" in showunoffintitle: - title = (title + ' (vid by ' + director + ')') - tracktitle = entry['tracktitle'] - tracktitle = tracktitle.replace(""", '"') - style = entry['style'] - if mycount == "pcount": - playcount = entry['pcount'] - elif mycount == "lcount": - playcount = entry['lcount'] - else: - playcount = entry['acount'] - #slug = entry['slug'] - if playLocalFile == "true": - description = "Provider: " + entry['provider'] + "\nprovider playcount: " + entry['pcount'] + "\nlikes: " + entry['likes'] + "\ndislikes: " + entry['dislikes'] + "\ncomments: " + entry['comments'] - else: - description = "Provider: " + entry['provider'] + "" - if "false" in entry['official'].lower(): - description = ( - "Unofficial Video by " + director + "\n" + description) - #link = entry['slug'] - fanart = entry['thumbnail'] - duration = entry['duration'] -# if (xbmcversion < 17): - li = xbmcgui.ListItem(title) -# else: -# li = xbmcgui.ListItem(title, offscreen=True) + # if len(entry) > 7: + mediatype = addon.getSetting("mediatype") + dateadded = datetime.utcfromtimestamp(float(entry["slug"])) + dateadded = str(dateadded) + slug = entry["slug"] + url = "%s?mode=play&url=%s" % (sys.argv[0], slug) + log(msg=str(url), level=xbmc.LOGDEBUG) - li.setInfo(type="video", infoLabels={ "mediatype": mediatype, "Title": title, "Originaltitle": tracktitle, "Genre": style, "Director": director, "PlotOutline": description, "Plot": description, "Tagline": style, "Artist": entry['artists'], "dateadded": dateadded, "playcount": playcount, "Duration": duration}) - li.setArt({'thumb': fanart}) - li.setProperty("fanart_image", fanart) - li.setProperty('IsPlayable', 'true') + channel = entry["artist"] + artist = entry["artist"] + artistslug = entry["artistslug"] + artistslugs = entry["artistslugs"] + artists = entry["artists"] + director = entry["director"] + title = entry["title"] + title = title.replace(""", '"') + if "false" in entry["official"].lower() and "true" in showunoffintitle: + title = title + " (vid by " + director + ")" + tracktitle = entry["tracktitle"] + tracktitle = tracktitle.replace(""", '"') + style = entry["style"] + keycamelot = entry["keycamelot"] + if mycount == "pcount": + playcount = entry["pcount"] + elif mycount == "lcount": + playcount = entry["lcount"] + else: + playcount = entry["acount"] + # slug = entry['slug'] + # if playLocalFile == "true": + description = "\n" + if "false" in entry["official"].lower(): + description = "\n\nUnofficial Video by " + director + description = ( + "Provider: " + + entry["provider"] + + "\nProvider playcount: " + + entry["pcount"] + + "\nDeveloper playcount: " + + entry["acount"] + + "\ncomments: " + + entry["comments"] + + "\nlikes: " + + entry["likes"] + + "\ndislikes: " + + entry["dislikes"] + + "\ncamelot: " + + entry["keycamelot"] + + description + ) + # else: + # description = "Provider: " + entry['provider'] + "" + # link = entry['slug'] + fanart = entry["thumbnail"] + duration = entry["duration"] + # if (xbmcversion < 17): + li = xbmcgui.ListItem() + # info_tag = ListItemInfoTag(li, 'video') + # info_tag.set_Title(title) + # else: + # li = xbmcgui.ListItem(title, offscreen=True) -# I could add all entries to the current, global playlist but it doesn't look right -# Imo at the end it's better if the user just uses "play from here" or the auto play next setting -# playlist.add(url=url, listitem=li) + li.setInfo( + type="video", + infoLabels={ + "mediatype": mediatype, + "Title": title, + "Originaltitle": tracktitle, + "Genre": style, + "Director": director, + "PlotOutline": description, + "Plot": description, + "Tagline": "", + "Artist": entry["artists"], + "dateadded": dateadded, + "playcount": playcount, + "Duration": duration, + }, + ) + li.setArt({"thumb": fanart}) + li.setProperty("fanart_image", fanart) + li.setProperty("IsPlayable", "true") - li.addContextMenuItems([ - (translation(30121),'XBMC.Container.Update(plugin://'+addonID+'/?mode=sortTitlesBy&url=%26start%3d'+slug+'%26limit%3drelated%26sort%3dnone)',), - (translation(30122),'XBMC.Container.Update(plugin://'+addonID+'/?mode=sortArtists&url=relartists%7C'+slug+')',) - ]) - ok = xbmcplugin.addDirectoryItem(handle=pluginhandle, url=url, listitem=li) + # I could add all entries to the current, global playlist but it doesn't look right + # Imo at the end it's better if the user just uses "play from here" or the auto play next setting + # playlist.add(url=url, listitem=li) + + li.addContextMenuItems( + [ + # (translation(30121),'Container.Update(plugin://'+addonID+'/?mode=sortTitlesBy&url=%26start%3d'+slug+'%26limit%3drelated%26sort%3dnone)',), + ( + translation(30121), + "Container.Update(plugin://" + + addonID + + "/?mode=sortTitlesBy&limit=related&start=" + + slug + + "&sort=none)", + ), + ( + translation(30122), + "Container.Update(plugin://" + + addonID + + "/?mode=sortArtists&limit=relartists&start=" + + quote_plus(str(artistslugs)) + + ")", + ), + ( + translation(30175), + "Container.Update(plugin://" + + addonID + + "/?mode=sortTitlesBy&limit=camelot&start=" + + quote_plus(str(keycamelot)) + + "&sort=random)", + ), + ( + translation(30176), + "Container.Update(plugin://" + + addonID + + "/?mode=sortTitlesBy&limit=camelot&start=" + + quote_plus(str(keycamelot)) + + "&sort=date)", + ), + ] + ) + ok = xbmcplugin.addDirectoryItem(handle=pluginhandle, url=url, listitem=li) return ok + def parameters_string_to_dict(parameters): paramDict = {} if parameters: paramPairs = parameters[1:].split("&") for paramsPair in paramPairs: - paramSplits = paramsPair.split('=') + paramSplits = paramsPair.split("=") if (len(paramSplits)) == 2: paramDict[paramSplits[0]] = paramSplits[1] return paramDict + params = parameters_string_to_dict(sys.argv[2]) -mode = urllib.unquote_plus(params.get('mode', '')).decode('utf-8') -url = urllib.unquote_plus(params.get('url', '')).decode('utf-8') +mode = urllib_parse.unquote_plus(params.get("mode", "")) +url = urllib_parse.unquote_plus(params.get("url", "")) +limit = urllib_parse.unquote_plus(params.get("limit", "")) +sort = urllib_parse.unquote_plus(params.get("sort", "")) +start = urllib_parse.unquote_plus(params.get("start", "")) +style = urllib_parse.unquote_plus(params.get("style", "")) +hit = urllib_parse.unquote_plus(params.get("hit", "")) +mycount = urllib_parse.unquote_plus(params.get("count", "")) +slug = urllib_parse.unquote_plus(params.get("slug", "")) -extraparams = parameters_string_to_dict(url) -limit = urllib.unquote_plus(extraparams.get('limit', '')).decode('utf-8') -sort = urllib.unquote_plus(extraparams.get('sort', '')).decode('utf-8') -start = urllib.unquote_plus(extraparams.get('start', '')).decode('utf-8') -start = str(start) -style = urllib.unquote_plus(extraparams.get('style', '')).decode('utf-8') -hit = urllib.unquote_plus(extraparams.get('hit', '')).decode('utf-8') -mycount = urllib.unquote_plus(extraparams.get('count', '')).decode('utf-8') -slug = urllib.unquote_plus(extraparams.get('slug', '')).decode('utf-8') +xbmc.log(msg=str(sys.argv), level=xbmc.LOGDEBUG) +xbmc.log(msg=str(mode), level=xbmc.LOGDEBUG) +xbmc.log(msg=str(url), level=xbmc.LOGDEBUG) +xbmc.log(msg=str(limit), level=xbmc.LOGDEBUG) +xbmc.log(msg=str(sort), level=xbmc.LOGDEBUG) +xbmc.log(msg=str(start), level=xbmc.LOGDEBUG) +xbmc.log(msg=str(style), level=xbmc.LOGDEBUG) - -#xbmc.log(msg=str(sys.argv), level=xbmc.LOGNOTICE) -#xbmc.log(msg=str(mode), level=xbmc.LOGNOTICE) -#xbmc.log(msg=str(url), level=xbmc.LOGNOTICE) -#xbmc.log(msg=str(limit), level=xbmc.LOGDEBUG) -#xbmc.log(msg=str(sort), level=xbmc.LOGDEBUG) -#xbmc.log(msg=str(start), level=xbmc.LOGDEBUG) -#xbmc.log(msg=str(style), level=xbmc.LOGNOTICE) - -if mode == 'updateData': +if mode == "updateData": updateData() -elif mode == 'alphabet': +elif mode == "alphabet": alphabet() -elif mode == 'showChannel': - showChannel(style, sort) -elif mode == 'search': +elif mode == "showStyle": + showStyle(style, sort) +elif mode == "search": search(url) -elif mode == 'sortTitleInitials': - sortTitleInitials(url) -elif mode == 'sortTitles': +elif mode == "sortTitles": sortTitles(url) -elif mode == 'sortArtists': - sortArtists(url) -elif mode == 'showArtist': +elif mode == "sortArtists": + sortArtists(limit, start) +elif mode == "showArtist": showArtist(style, limit, start) -elif mode == 'sortTitlesBy': +elif mode == "sortTitlesBy": sortTitlesBy(limit, sort, start) -elif mode == 'searchDate': +elif mode == "searchDate": searchDate(url) -elif mode == 'mainsorted': +elif mode == "mainsorted": mainsorted() -elif mode == 'mainrandom': +elif mode == "mainrandom": mainrandom() -elif mode == 'numbers': +elif mode == "numbers": numbers() -elif mode == 'countrycodes': +elif mode == "countrycodes": countrycodes() -elif mode == 'artists': +elif mode == "artists": artists() -elif mode == 'styles': +elif mode == "styles": styles() -elif mode == 'related': - related(limit) -elif mode == 'play': +elif mode == "camelot": + camelot() +elif mode == "play": play(url) +elif mode == "opensettings": + opensettings() +elif mode == "openytdlsettings": + openytdlsettings() else: main() + +# we have no psutil in CoreELEC Kodi so we kill the debugging process the ugly way +if str(addon.getSetting("debugvscode")) == "true": + import os + import signal + import subprocess + + p = subprocess.Popen(["ps", "a"], stdout=subprocess.PIPE) + out, err = p.communicate() + for line in out.splitlines(): + if "script.module.debugpy" in str(line): + pid = int(line.split(None, 1)[0]) + os.kill(pid, signal.SIGKILL) diff --git a/resources/images/fanart_Glitch.jpg b/resources/images/fanart_Glitch.jpg index 5304899..9fbf2f8 100644 Binary files a/resources/images/fanart_Glitch.jpg and b/resources/images/fanart_Glitch.jpg differ diff --git a/resources/images/fanart_Swinged.jpg b/resources/images/fanart_Swinged.jpg index cd4562f..0b7b74e 100644 Binary files a/resources/images/fanart_Swinged.jpg and b/resources/images/fanart_Swinged.jpg differ diff --git a/resources/img/camelot/1A.png b/resources/img/camelot/1A.png new file mode 100644 index 0000000..9888be6 Binary files /dev/null and b/resources/img/camelot/1A.png differ diff --git a/resources/img/camelot/camelot.png b/resources/img/camelot/camelot.png new file mode 100644 index 0000000..ef69cd1 Binary files /dev/null and b/resources/img/camelot/camelot.png differ diff --git a/resources/language/resource.language.de_de/strings.po b/resources/language/resource.language.de_de/strings.po index be0308c..490b9f4 100644 --- a/resources/language/resource.language.de_de/strings.po +++ b/resources/language/resource.language.de_de/strings.po @@ -17,8 +17,8 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgctxt "#30001" -msgid "Global Search" -msgstr "Globale Suche" +msgid "Search" +msgstr "Suchen" msgctxt "#30002" msgid "Search" @@ -44,6 +44,10 @@ msgctxt "#30008" msgid "Incoming Hits" msgstr "Neueste Hits" +msgctxt "#30009" +msgid "Settings" +msgstr "Einstellungen" + msgctxt "#30011" msgid "Random" msgstr "Zufällig" @@ -89,8 +93,8 @@ msgid "Hits, grouped by style" msgstr "Hits, gruppiert nach Musikstil" msgctxt "#30022" -msgid "Genre" -msgstr "Genre" +msgid "Style" +msgstr "Style" msgctxt "#30024" msgid "Random Musly Playlist" @@ -133,13 +137,13 @@ msgid "Hits sorted by date" msgstr "Hits nach Datum sortiert" msgctxt "#30034" -msgid "Last Year" -msgstr "Letztes Jahr" - -msgctxt "#30035" msgid "Last Year Hits" msgstr "Letztes Jahr Hits" +msgctxt "#30035" +msgid "Last Year" +msgstr "Letztes Jahr" + msgctxt "#30036" msgid "Next Page" msgstr "Nächste Seite" @@ -177,12 +181,12 @@ msgid "Play local file if available" msgstr "Lokale Datei abspielen wenn vorhanden" msgctxt "#30105" -msgid "Use youtube-dl Control Addon (slower)" -msgstr "Nutze youtube-dl Control Addon (langsamer)" +msgid "Use youtube-dl Control Add-On (slower startup)" +msgstr "Nutze youtube-dl Control Add-On (startet langsamer)" msgctxt "#30106" -msgid "Mediatype (maybe useful for skins without proper musicvideo support)" -msgstr "Mediatype (eventuell nützlich bei Skins ohne guter Musikvideo-Unterstützung)" +msgid "Mediatype" +msgstr "Mediatype" msgctxt "#30107" msgid "Only useful on the addon developer's machine (TODO)" @@ -259,3 +263,211 @@ msgstr "unbekannt" msgctxt "#30125" msgid "Folder" msgstr "Verzeichnisse" + +msgctxt "#30126" +msgid "Threshold for reducing random selections" +msgstr "Schwellwert zum einschränken von Zufallsauswahlen" + +msgctxt "#30127" +msgid "Play Youtube Videos with Invidious Add-On" +msgstr "Nutze Invidious Add-On für Youtube-Videos" + +msgctxt "#30128" +msgid "Play Youtube Videos with Invidious Add-On (DASH-Video must be disabled in Add-On)" +msgstr "Nutze Invidious Add-On für Youtube-Videos (DASH-Video muss in Add-On dektiviert sein)" + +msgctxt "#30129" +msgid "youtube-dl Control Settings" +msgstr "youtube-dl Control Einstellungen" + +msgctxt "#30130" +msgid "Resolver" +msgstr "Resolver" + +msgctxt "#30131" +msgid "Youtube" +msgstr "Youtube" + +msgctxt "#30132" +msgid "Vimeo" +msgstr "Vimeo" + +msgctxt "#30133" +msgid "Dailymotion" +msgstr "Dailymotion" + +msgctxt "#30133" +msgid "Facebook" +msgstr "Facebook" + +msgctxt "#30135" +msgid "Vevo" +msgstr "Vevo" + +msgctxt "#30136" +msgid "Invidious Settings" +msgstr "Invidious Einstellungen" + +msgctxt "#30137" +msgid "Preferred Video Provider" +msgstr "Bevorzugter Video Provider" + +msgctxt "#30138" +msgid "Vimeo sometimes has better Video Quality and in general a higher bitrate" +msgstr "Die Video-Qualität bei Vimeo ist meistens besser und die Bitrate fast immer höher" + +msgctxt "#30139" +msgid "Show Notifications" +msgstr "Benachrichtigungen anzeigen" + +msgctxt "#30140" +msgid "Download successfull" +msgstr "Download erfolgreich" + +msgctxt "#30141" +msgid "Could not download video data!" +msgstr "Konnte Video-Daten nicht herunterladen!" + +msgctxt "#30142" +msgid "Duration" +msgstr "Länge" + +msgctxt "#30143" +msgid "Enable VS Code debugging" +msgstr "Aktiviere VS Code debugging" + +msgctxt "#30144" +msgid "Exact artist selection" +msgstr "Exakte Künstler-Auswahl" + +msgctxt "#30145" +msgid "Recursive artist selection" +msgstr "Rekursive Künstler-Auswahl" + +msgctxt "#30145" +msgid "Recursive artist selection" +msgstr "Rekursive Künstler-Auswahl" + +msgctxt "#30146" +msgid "Age restricted videos" +msgstr "Altersbeschränkte Videos" + +msgctxt "#30147" +msgid "By Color" +msgstr "nach Farbgruppe" + +msgctxt "#30148" +msgid "Last 3 year hits" +msgstr "Letzte 3 Jahre Hits" + +msgctxt "#30149" +msgid "Last 3 years" +msgstr "Letzte 3 Jahre" + +msgctxt "#30150" +msgid "A-Flat Minor (Camelot 1A)" +msgstr "as-moll (Camelot 1A)" + +msgctxt "#30151" +msgid "A Major (Camelot 1B)" +msgstr "A-Dur (Camelot 1B)" + +msgctxt "#30152" +msgid "E-Flat Minor (Camelot 2A)" +msgstr "es-moll (Camelot 2A)" + +msgctxt "#30153" +msgid "F-Sharp Major (Camelot 2B)" +msgstr "Fis-Dur (Camelot 2B)" + +msgctxt "#30154" +msgid "B-Flat Minor (Camelot 3A)" +msgstr "b-moll (Camelot 3A)" + +msgctxt "#30155" +msgid "D-Flat Major (Camelot 3B)" +msgstr "Des-Dur (Camelot 3B)" + +msgctxt "#30156" +msgid "F Minor (Camelot 4A)" +msgstr "f-moll (Camelot 4A)" + +msgctxt "#30157" +msgid "A-Flat Major (Camelot 4B)" +msgstr "As-Dur (Camelot 4B)" + +msgctxt "#30158" +msgid "C Minor (Camelot 5A)" +msgstr "c-moll (Camelot 5A)" + +msgctxt "#30159" +msgid "F-Flat Major (Camelot 5B)" +msgstr "Es-Dur (Camelot 5B)" + +msgctxt "#30160" +msgid "G Minor (Camelot 6A)" +msgstr "g-moll (Camelot 6A)" + +msgctxt "#30161" +msgid "B-Flat Major (Camelot 6B)" +msgstr "B-Dur (Camelot 6B)" + +msgctxt "#30162" +msgid "D Minor (Camelot 7A)" +msgstr "d-moll (Camelot 7A)" + +msgctxt "#30163" +msgid "F Major (Camelot 7B)" +msgstr "F-Dur (Camelot 7B)" + +msgctxt "#30164" +msgid "A Minor (Camelot 8A)" +msgstr "a-moll (Camelot 8A)" + +msgctxt "#30165" +msgid "C Major (Camelot 8B)" +msgstr "C-Dur (Camelot 8B)" + +msgctxt "#30166" +msgid "E Minor (Camelot 9A)" +msgstr "e-moll (Camelot 9A)" + +msgctxt "#30167" +msgid "G Major (Camelot 9B)" +msgstr "G-Dur (Camelot 9B)" + +msgctxt "#30168" +msgid "B Minor (Camelot 10A)" +msgstr "h-moll (Camelot 10A)" + +msgctxt "#30169" +msgid "D Major (Camelot 10B)" +msgstr "D-Dur (Camelot 10B)" + +msgctxt "#30170" +msgid "F-Sharp Major (Camelot 11A)" +msgstr "Fis-Dur (Camelot 11A)" + +msgctxt "#30171" +msgid "A Major (Camelot 11B)" +msgstr "A-Dur (Camelot 11B)" + +msgctxt "#30172" +msgid "D-Flat Minor (Camelot 12A)" +msgstr "des-moll (Camelot 12A)" + +msgctxt "#30173" +msgid "E Major (Camelot 12B)" +msgstr "E-Dur (Camelot 12B)" + +msgctxt "#30174" +msgid "By Tonality" +msgstr "Nach Tonart" + +msgctxt "#30175" +msgid "More in this Tonality, random" +msgstr "Mehr in dieser Tonart, zufällig" + +msgctxt "#30176" +msgid "More in this Tonality" +msgstr "Mehr in dieser Tonart" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 50e9b15..f336e0b 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -17,7 +17,7 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgctxt "#30001" -msgid "Global Search" +msgid "Search" msgstr "" msgctxt "#30002" @@ -44,6 +44,10 @@ msgctxt "#30008" msgid "Incoming Hits" msgstr "" +msgctxt "#30009" +msgid "Settings" +msgstr "" + msgctxt "#30011" msgid "Random" msgstr "" @@ -89,7 +93,7 @@ msgid "Hits, grouped by style" msgstr "" msgctxt "#30022" -msgid "Genre" +msgid "Style" msgstr "" msgctxt "#30024" @@ -133,13 +137,14 @@ msgid "Hits sorted by date" msgstr "" msgctxt "#30034" -msgid "Last Year" +msgid "Last Year Hits" msgstr "" msgctxt "#30035" -msgid "Last Year Hits" +msgid "Last Year" msgstr "" + msgctxt "#30036" msgid "Next Page" msgstr "" @@ -177,11 +182,11 @@ msgid "Play local file if available" msgstr "" msgctxt "#30105" -msgid "Use youtube-dl Control Addon (slower)" +msgid "Use youtube-dl Control Add-On (slower startup)" msgstr "" msgctxt "#30106" -msgid "Mediatype (maybe useful for skins without proper musicvideo support)" +msgid "Mediatype" msgstr "" msgctxt "#30107" @@ -259,3 +264,208 @@ msgstr "" msgctxt "#30125" msgid "Folder" msgstr "" + +msgctxt "#30126" +msgid "Threshold for reducing random selections" +msgstr "" + +msgctxt "#30127" +msgid "Play Youtube Videos with Invidious Add-On" +msgstr "" + +msgctxt "#30128" +msgid "Play Youtube Videos with Invidious Addon (DASH-Video must be disabled in Add-On)" +msgstr "" + +msgctxt "#30129" +msgid "youtube-dl Control Settings" +msgstr "" + +msgctxt "#30130" +msgid "Resolver" +msgstr "" + +msgctxt "#30131" +msgid "Youtube" +msgstr "" + +msgctxt "#30132" +msgid "Vimeo" +msgstr "" + +msgctxt "#30133" +msgid "Dailymotion" +msgstr "" + +msgctxt "#30134" +msgid "Facebook" +msgstr "" + +msgctxt "#30135" +msgid "Vevo" +msgstr "" + +msgctxt "#30136" +msgid "Invidious Settings" +msgstr "" + +msgctxt "#30137" +msgid "Preferred Video Provider" +msgstr "" + +msgctxt "#30138" +msgid "Vimeo sometimes has better Video Quality and in general a higher bitrate" +msgstr "" + +msgctxt "#30139" +msgid "Show Notifications" +msgstr "" + +msgctxt "#30140" +msgid "Download successfull" +msgstr "" + +msgctxt "#30141" +msgid "Could not download video data!" +msgstr "" + +msgctxt "#30142" +msgid "Duration" +msgstr "" + +msgctxt "#30143" +msgid "Enable VS Code debugging" +msgstr "" + +msgctxt "#30144" +msgid "Exact artist selection" +msgstr "" + +msgctxt "#30145" +msgid "Recursive artist selection" +msgstr "" + +msgctxt "#30146" +msgid "Age restricted videos" +msgstr "" + +msgctxt "#30147" +msgid "By Color" +msgstr "" + + +msgctxt "#30148" +msgid "Last 3 year hits" +msgstr "" + +msgctxt "#30149" +msgid "Last 3 years" +msgstr "" + +msgctxt "#30150" +msgid "A-Flat Minor (Camelot 1A)" +msgstr "" + +msgctxt "#30151" +msgid "A Major (Camelot 1B)" +msgstr "" + +msgctxt "#30152" +msgid "E-Flat Minor (Camelot 2A)" +msgstr "" + +msgctxt "#30153" +msgid "F-Sharp Major (Camelot 2B)" +msgstr "" + +msgctxt "#30154" +msgid "B-Flat Minor (Camelot 3A)" +msgstr "" + +msgctxt "#30155" +msgid "D-Flat Major (Camelot 3B)" +msgstr "" + +msgctxt "#30156" +msgid "F Minor (Camelot 4A)" +msgstr "" + +msgctxt "#30157" +msgid "A-Flat Major (Camelot 4B)" +msgstr "" + +msgctxt "#30158" +msgid "C Minor (Camelot 5A)" +msgstr "" + +msgctxt "#30159" +msgid "F-Flat Major (Camelot 5B)" +msgstr "" + +msgctxt "#30160" +msgid "G Minor (Camelot 6A)" +msgstr "" + +msgctxt "#30161" +msgid "B-Flat Major (Camelot 6B)" +msgstr "" + +msgctxt "#30162" +msgid "D Minor (Camelot 7A)" +msgstr "" + +msgctxt "#30163" +msgid "F Major (Camelot 7B)" +msgstr "" + +msgctxt "#30164" +msgid "A Minor (Camelot 8A)" +msgstr "" + +msgctxt "#30165" +msgid "C Major (Camelot 8B)" +msgstr "" + +msgctxt "#30166" +msgid "E Minor (Camelot 9A)" +msgstr "" + +msgctxt "#30167" +msgid "G Major (Camelot 9B)" +msgstr "" + +msgctxt "#30168" +msgid "B Minor (Camelot 10A)" +msgstr "" + +msgctxt "#30169" +msgid "D Major (Camelot 10B)" +msgstr "" + +msgctxt "#30170" +msgid "F-Sharp Major (Camelot 11A)" +msgstr "" + +msgctxt "#30171" +msgid "A Major (Camelot 11B)" +msgstr "" + +msgctxt "#30172" +msgid "D-Flat Minor (Camelot 12A)" +msgstr "" + +msgctxt "#30173" +msgid "E Major (Camelot 12B)" +msgstr "" + +msgctxt "#30174" +msgid "By Tonality" +msgstr "" + +msgctxt "#30175" +msgid "More in this Tonality, random" +msgstr "" + +msgctxt "#30176" +msgid "More in this Tonality" +msgstr "" diff --git a/resources/settings.xml b/resources/settings.xml index e7b593d..408c7cc 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -1,36 +1,52 @@ - + - - - - - - + + + + + + - + + + + + + + + + + + + + + + + - + - + - - - - - - - - - - + + + + + + + + + + +