diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/addon.xml b/addon.xml index febdefe..f0f0db8 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/cache.py b/cache.py deleted file mode 100644 index 30a478d..0000000 --- a/cache.py +++ /dev/null @@ -1,87 +0,0 @@ -import xbmcaddon - -import sqlite3 as sql -import json - -from pathlib import Path -from xbmcvfs import translatePath -from time import time - -def get_filename(): - addon = xbmcaddon.Addon() - cache_path = Path(translatePath(addon.getAddonInfo('profile'))) - cache_path.mkdir(parents=True, exist_ok=True) - fname = str(cache_path/'cache.db') - return fname - -class Cache: - def __init__(self): - self.file = get_filename() - - def create(self): - conn = sql.connect(self.file) - table = ''' - CREATE TABLE IF NOT EXISTS categories ( - id TEXT PRIMARY KEY NOT NULL, - expires INTEGER, - data JSON ); - ''' - conn.execute(table) - conn.commit() - conn.close() - - def get(self, category): - self.delete_expired() - conn = sql.connect(self.file) - cur = conn.cursor() - - data = None - - cur.execute(f'SELECT data FROM categories WHERE id = ?', (category,),) - results = cur.fetchall() - if len(results) > 0: - data = json.loads(results[0][0]) - - cur.close() - conn.close() - - return data - - def get_all(self): - conn = sql.connect(self.file) - cur = conn.cursor() - - data = [] - - cur.execute(f'SELECT id, data FROM categories') - results = cur.fetchall() - for result in results: - data.append({'id': str(result[0]), 'json': json.loads(result[1])}) - - cur.close() - conn.close() - - return data - - - def insert(self, category, data, expire_after=14400.0): - conn = sql.connect(self.file) - #todo: add settings - expires_at = round(time() + expire_after) - conn.execute(f'INSERT OR REPLACE INTO categories (id,expires,data) VALUES (?,?,?)', (category, expires_at, json.dumps(data),),) - conn.commit() - conn.close() - - - def delete_expired(self): - conn = sql.connect(self.file) - conn.execute(f'DELETE FROM categories WHERE expires <= ?', (round(time()),),) - conn.commit() - conn.close() - - - def flush(self): - conn = sql.connect(self.file) - conn.execute(f'DELETE FROM categories') - conn.commit() - conn.close() \ No newline at end of file diff --git a/favourites.py b/favourites.py deleted file mode 100644 index 9492a4c..0000000 --- a/favourites.py +++ /dev/null @@ -1,29 +0,0 @@ -import xbmcgui - -from mylist import MyList - -def show_favourites(): - series = MyList().select_favourites() - list = [] - - for serie in series: - li = xbmcgui.ListItem(serie[2]) - list.append(li) - - dialog = xbmcgui.Dialog() - selected_index = dialog.select("MyList", list) - if selected_index >= 0: - li = list[selected_index] - serie = series[selected_index] - - thumb = 'https://statics.tver.jp/images/content/thumbnail/series/small/{}.jpg'.format(serie[0]) - vid_info = li.getVideoInfoTag() - vid_info.setTitle(serie[2]) - vid_info.setGenres([serie[1]]) - vid_info.setMediaType('tvshow') - - li.setArt({'thumb': thumb, 'icon': thumb, 'fanart': thumb}) - li.setProperty('IsPlayable', 'false') - - dialog = xbmcgui.Dialog() - dialog.info(li) \ No newline at end of file diff --git a/lib/__init__.py b/lib/__init__.py index e69de29..d4fbcf3 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -0,0 +1,9 @@ +from lib.utils import * +from lib.db import database + +from lib.cache import Cache +from lib.tver import * +from lib.favourites import Favourites +from lib.watcher import Watcher +from lib.mylist import MyList + diff --git a/lib/cache.py b/lib/cache.py new file mode 100644 index 0000000..88250f7 --- /dev/null +++ b/lib/cache.py @@ -0,0 +1,53 @@ +import sqlite3 as sql +import json + +from time import time +from lib import database + +class Cache: + def create(self): + with sql.connect(database()) as conn: + table = ''' + CREATE TABLE IF NOT EXISTS categories ( + id TEXT PRIMARY KEY NOT NULL, + expires INTEGER, + data JSON ); + ''' + conn.execute(table) + + def get(self, category): + data = None + self.delete_expired() + + with sql.connect(database()) as conn: + cur = conn.execute(f'SELECT data FROM categories WHERE id = ?', (category,),) + results = cur.fetchall() + + if len(results) > 0: + data = json.loads(results[0][0]) + + return data + + def get_all(self): + data = [] + + with sql.connect(database()) as conn: + cur = conn.execute(f'SELECT id, data FROM categories') + results = cur.fetchall() + + for result in results: + data.append({'id': str(result[0]), 'json': json.loads(result[1])}) + + return data + + + def insert(self, category, data, expire_after=14400.0): + expires_at = round(time() + expire_after) + + with sql.connect(database()) as conn: + conn.execute(f'INSERT OR REPLACE INTO categories (id,expires,data) VALUES (?,?,?)', (category, expires_at, json.dumps(data),),) + + + def delete_expired(self): + with sql.connect(database()) as conn: + conn.execute(f'DELETE FROM categories WHERE expires <= ?', (round(time()),),) \ No newline at end of file diff --git a/lib/db.py b/lib/db.py new file mode 100644 index 0000000..692872e --- /dev/null +++ b/lib/db.py @@ -0,0 +1,18 @@ +import xbmcaddon +from pathlib import Path +from xbmcvfs import translatePath + +def get_filename(): + addon = xbmcaddon.Addon() + cache_path = Path(translatePath(addon.getAddonInfo('profile'))) + cache_path.mkdir(parents=True, exist_ok=True) + fname = str(cache_path/'tver.db') + return fname + +_DB = get_filename() + +def database(): + return _DB + + + diff --git a/lib/favourites.py b/lib/favourites.py new file mode 100644 index 0000000..af05e1a --- /dev/null +++ b/lib/favourites.py @@ -0,0 +1,60 @@ +import sqlite3 as sql +import xbmcgui + +from lib.tver import URL_VIDEO_PICTURE +from lib import database, localize + +class Favourites: + def create(self): + with sql.connect(database()) as conn: + table = ''' + CREATE TABLE IF NOT EXISTS favourites ( + id TEXT PRIMARY KEY NOT NULL, + category TEXT, + title TEXT + ); + ''' + conn.execute(table) + + def insert(self,category,series,title): + with sql.connect(database()) as conn: + conn.execute(f'INSERT OR REPLACE INTO favourites (id,category,title) VALUES (?,?,?)', (series, category, title,),) + + def select(self): + favs = [] + + with sql.connect(database()) as conn: + cur = conn.execute(f'SELECT id, category, title FROM favourites') + favs = cur.fetchall() + + return favs + + def delete(self, series): + with sql.connect(database()) as conn: + conn.execute(f'DELETE FROM favourites WHERE id = ?', (series,),) + + def list(self): + series = self.select() + list = [] + + for serie in series: + li = xbmcgui.ListItem(serie[2]) + list.append(li) + + dialog = xbmcgui.Dialog() + selected_index = dialog.select(localize(30002), list) + if selected_index >= 0: + li = list[selected_index] + serie = series[selected_index] + + thumb = URL_VIDEO_PICTURE.format('series', serie[0]) + vid_info = li.getVideoInfoTag() + vid_info.setTitle(serie[2]) + vid_info.setGenres([serie[1]]) + vid_info.setMediaType('tvshow') + + li.setArt({'thumb': thumb, 'icon': thumb, 'fanart': thumb}) + li.setProperty('IsPlayable', 'false') + + dialog = xbmcgui.Dialog() + dialog.info(li) \ No newline at end of file diff --git a/lib/mylist.py b/lib/mylist.py new file mode 100644 index 0000000..eb4cbfe --- /dev/null +++ b/lib/mylist.py @@ -0,0 +1,121 @@ +import sys +import sqlite3 as sql + +from time import time + +from lib.tver import URL_VIDEO_PICTURE, URL_VIDEO_WEBSITE +from lib import Cache, Watcher, Favourites, strip_or_none, get_url, database + +from urllib.parse import parse_qsl + +class MyList: + def __init__(self): + self.favourites = Favourites() + + def create(self): + with sql.connect(database()) as conn: + table = ''' + CREATE TABLE IF NOT EXISTS mylist ( + id TEXT PRIMARY KEY NOT NULL, + expires INTEGER, + vid_type TEXT, + title TEXT, + series TEXT + ); + ''' + conn.execute(table) + + def build(self): + mylist = [item[0] for item in self.select()] + favourites = self.favourites.select() + + categories = list(set([fav[1] for fav in favourites])) + series = [fav[0] for fav in favourites] + + for category in categories: + cached_episodes = Cache().get(category) + + for episode in cached_episodes['result']['contents']: + series_id = episode['content']['seriesID'] + if series_id in series: + video_id = episode['content']['id'] + if video_id in mylist: + continue + self.insert(episode['type'],episode['content']) + + + def add(self,category,series,title): + self.favourites.insert(category,series,title) + self.build() + + def remove(self,series): + self.favourites.delete(series) + self.delete(series) + + def get(self): + self.delete_expired() + mylist = self.select() + + watched = [str(dict(parse_qsl(entry))['video']).split('/')[-1] for entry in Watcher().select_watched_from_list([get_url(action='play', video=URL_VIDEO_WEBSITE.format(item[1], item[0])) for item in mylist])] + + episodes = [] + + for (video_id, video_type, title, series_id, series_title) in mylist: + if video_id in watched: + continue + label = ' '.join(filter(None, [strip_or_none(series_title), strip_or_none(title)])) + episodes.append({ 'name': label, + 'series': series_id, + 'thumb': URL_VIDEO_PICTURE.format(video_type, video_id), + 'video': URL_VIDEO_WEBSITE.format(video_type, video_id), + 'genre': None, + 'series_title': series_title }) + + return episodes + + def get_random_pic(self): + pic = None + + with sql.connect(database()) as conn: + stmt = f'SELECT id, vid_type FROM mylist ORDER BY RANDOM() LIMIT 1' + cur = conn.execute(stmt) + results = cur.fetchall() + + if results: + pic = URL_VIDEO_PICTURE.format(results[0][1], results[0][0]) + + return pic + + def select(self): + results = [] + + with sql.connect(database()) as conn: + stmt = ''' + SELECT my.id, + my.vid_type, + my.title, + fav.id as series, + fav.title as series_title + FROM mylist as my + INNER JOIN favourites as fav on fav.id = my.series + ''' + cur = conn.execute(stmt) + results = cur.fetchall() + + return results + + def insert(self, type, content): + with sql.connect(database()) as conn: + conn.execute(''' + INSERT OR REPLACE INTO mylist (id,expires,vid_type,title,series) + VALUES (?,?,?,?,?) + ''', + (content['id'], content['endAt'], type, content['title'], content['seriesID'], ),) + + def delete(self, series): + with sql.connect(database()) as conn: + conn.execute(f'DELETE FROM mylist WHERE series = ?', (series,),) + + def delete_expired(self): + with sql.connect(database()) as conn: + conn.execute(f'DELETE FROM mylist WHERE expires <= ?', (round(time()),),) \ No newline at end of file diff --git a/tver.py b/lib/tver.py similarity index 66% rename from tver.py rename to lib/tver.py index aa33684..f3279f4 100644 --- a/tver.py +++ b/lib/tver.py @@ -1,8 +1,6 @@ import requests -from cache import Cache -from watcher import Watcher -from utils import get_random_ua, strip_or_none, get_custom_img_path, find_episode, localize -from urllib.parse import parse_qsl +from lib import Cache +from lib import get_random_ua, strip_or_none, get_custom_img_path, localize URL_TOKEN_SERVICE = 'https://platform-api.tver.jp/v2/api/platform_users/browser/create' URL_TAG_SEARCH_WS = 'https://platform-api.tver.jp/service/api/v1/callTagSearch/{}' @@ -71,34 +69,3 @@ def get_episodes(category): 'series_title': series_title }) return episodes - -def get_watching_episodes(): - episodes = [] - - watching = Watcher().select() - - if watching: - full_cache = Cache().get_all() - - for row in watching: - file = row[0] - params = dict(parse_qsl(file)) - tver_url = params['video'] - video_id = str(tver_url).split('/')[-1] - - episode = find_episode(full_cache, video_id) - if episode: - label = ' '.join(filter(None, [strip_or_none(episode['seriesTitle']), strip_or_none(episode['title'])])) - episodes.append({ 'name': label, - 'series': None, - 'thumb': URL_VIDEO_PICTURE.format('episode', video_id), - 'video': tver_url, - 'genre': None }) - else: - #video got pulled by tver - episodes.append({ 'name': video_id, - 'series': None, - 'thumb': URL_VIDEO_PICTURE.format('episode', video_id), - 'video': tver_url, - 'genre': None }) - return episodes diff --git a/utils.py b/lib/utils.py similarity index 92% rename from utils.py rename to lib/utils.py index 10399b1..bbb5d91 100644 --- a/utils.py +++ b/lib/utils.py @@ -29,25 +29,13 @@ def get_url(**kwargs): return '{}?{}'.format(_URL, urlencode(kwargs)) -def debug(content): - log(content, xbmc.LOGDEBUG) - - -def notice(content): - log(content, xbmc.LOGINFO) - - def log(msg, level=xbmc.LOGINFO): - addon = xbmcaddon.Addon() - addonID = addon.getAddonInfo('id') + addonID = xbmcaddon.Addon().getAddonInfo('id') xbmc.log('%s: %s' % (addonID, msg), level) def show_info(message): xbmcgui.Dialog().notification(localize(30000), message, xbmcgui.NOTIFICATION_INFO, 5000) -def show_error(message): - xbmcgui.Dialog().notification(localize(30000), message, xbmcgui.NOTIFICATION_ERROR, 5000) - def get_random_ua(): return user_agents[randint(0, len(user_agents) - 1)] @@ -130,9 +118,9 @@ def lookup_db(db_name): return None -def find_episode(full_cache, episode_id): +def find_episode(caches, episode_id): episode_json = None - for cache in full_cache: + for cache in caches: for content in cache['json']['result']['contents']: content = content['content'] if content['id'] == episode_id: diff --git a/lib/watcher.py b/lib/watcher.py new file mode 100644 index 0000000..90d9e87 --- /dev/null +++ b/lib/watcher.py @@ -0,0 +1,80 @@ +import sys +import sqlite3 as sql + +from urllib.parse import parse_qsl +from lib import Cache, lookup_db, find_episode, strip_or_none, URL_VIDEO_PICTURE + +_URL = sys.argv[0] +_DB = lookup_db("MyVideos") + +class Watcher: + def is_watching(self): + return bool( self.select() ) + + def select(self): + results = [] + + with sql.connect(_DB) as conn: + stmt = ''' + SELECT f.strFilename, + b.timeInSeconds, + b.totalTimeInSeconds + FROM bookmark as b + INNER JOIN files as f ON f.idFile = b.idFile + INNER JOIN path as p ON p.idPath = f.idPath + WHERE f.playCount IS NULL + AND p.strPath = ? + ''' + cur = conn.execute(stmt, (_URL,),) + results = cur.fetchall() + + return results + + def select_watched_from_list(self, list): + if not list: + return [] + + placeholders = ', '.join(['?'] * len(list)) + watched = [] + + with sql.connect(_DB) as conn: + cur = conn.execute(''' + SELECT strFilename + FROM files + WHERE strFilename IN ({}) + AND playCount IS NOT NULL + '''.format(placeholders), list) + results = cur.fetchall() + + watched = [result[0] for result in results] + + return watched + + def get_watching_episodes(self): + episodes = [] + + watching = Watcher().select() + + if watching: + caches = Cache().get_all() + + for row in watching: + file = row[0] + params = dict(parse_qsl(file)) + tver_url = params['video'] + video_id = str(tver_url).split('/')[-1] + + #in case the video was pulled from TVer + label = video_id + + episode = find_episode(caches, video_id) + if episode: + label = ' '.join(filter(None, [strip_or_none(episode['seriesTitle']), strip_or_none(episode['title'])])) + + episodes.append({ 'name': label, + 'series': None, + 'thumb': URL_VIDEO_PICTURE.format('episode', video_id), + 'video': tver_url, + 'genre': None }) + + return episodes \ No newline at end of file diff --git a/mylist.py b/mylist.py deleted file mode 100644 index abe71e5..0000000 --- a/mylist.py +++ /dev/null @@ -1,179 +0,0 @@ -import sys -import sqlite3 as sql - -from time import time -from cache import Cache, get_filename -from tver import URL_VIDEO_PICTURE, URL_VIDEO_WEBSITE -from watcher import Watcher -from utils import strip_or_none, get_url -from urllib.parse import parse_qsl - -_URL = sys.argv[0] - -class MyList: - def __init__(self): - self.file = get_filename() - - def create(self): - conn = sql.connect(self.file) - table = ''' - CREATE TABLE IF NOT EXISTS mylist ( - id TEXT PRIMARY KEY NOT NULL, - expires INTEGER, - vid_type TEXT, - title TEXT, - series TEXT - ); - ''' - conn.execute(table) - conn.commit() - - table = ''' - CREATE TABLE IF NOT EXISTS favourites ( - id TEXT PRIMARY KEY NOT NULL, - category TEXT, - title TEXT - ); - ''' - conn.execute(table) - conn.commit() - conn.close() - - def build(self): - mylist = [item[0] for item in self.select()] - - favourites = self.select_favourites() - categories = list(set([fav[1] for fav in favourites])) - series = [fav[0] for fav in favourites] - - for category in categories: - cached_episodes = Cache().get(category) - - for episode in cached_episodes['result']['contents']: - series_id = episode['content']['seriesID'] - if series_id in series: - video_id = episode['content']['id'] - if video_id in mylist: - continue - self.insert(episode['type'],episode['content']) - - - def add(self,category,series,title): - conn = sql.connect(self.file) - conn.execute(f'INSERT OR REPLACE INTO favourites (id,category,title) VALUES (?,?,?)', (series, category, title,),) - conn.commit() - conn.close() - #rebuild mylist - self.build() - - def remove(self,series): - conn = sql.connect(self.file) - conn.execute(f'DELETE FROM favourites WHERE id = ?', (series,),) - conn.commit() - conn.execute(f'DELETE FROM mylist WHERE series = ?', (series,),) - conn.commit() - conn.close() - - def select_favourites(self): - conn = sql.connect(self.file) - cur = conn.cursor() - - stmt = ''' - SELECT id, - category, - title - FROM favourites - ''' - cur.execute(stmt) - - favs = cur.fetchall() - - cur.close() - conn.close() - - return favs - - def select(self): - conn = sql.connect(self.file) - cur = conn.cursor() - - stmt = ''' - SELECT my.id, - my.vid_type, - my.title, - fav.id as series, - fav.title as series_title - FROM mylist as my - INNER JOIN favourites as fav on fav.id = my.series - ''' - cur.execute(stmt) - - results = cur.fetchall() - - cur.close() - conn.close() - - return results - - def insert(self, type, content): - conn = sql.connect(self.file) - - conn.execute(''' - INSERT OR REPLACE INTO mylist (id,expires,vid_type,title,series) - VALUES (?,?,?,?,?) - ''', - (content['id'], content['endAt'], type, content['title'], content['seriesID'], ),) - - conn.commit() - conn.close() - - def get(self): - self.delete_expired() - mylist = self.select() - - watched = [str(dict(parse_qsl(entry))['video']).split('/')[-1] for entry in Watcher().select_watched_from_list([get_url(action='play', video=URL_VIDEO_WEBSITE.format(item[1], item[0])) for item in mylist])] - - episodes = [] - - for (video_id, video_type, title, series_id, series_title) in mylist: - if video_id in watched: - continue - label = ' '.join(filter(None, [strip_or_none(series_title), strip_or_none(title)])) - episodes.append({ 'name': label, - 'series': series_id, - 'thumb': URL_VIDEO_PICTURE.format(video_type, video_id), - 'video': URL_VIDEO_WEBSITE.format(video_type, video_id), - 'genre': None, - 'series_title': series_title }) - - return episodes - - def get_random_pic(self): - pic = None - - conn = sql.connect(self.file) - cur = conn.cursor() - - stmt = f'SELECT id, vid_type FROM mylist ORDER BY RANDOM() LIMIT 1' - cur.execute(stmt) - - results = cur.fetchall() - - cur.close() - conn.close() - if results: - pic = URL_VIDEO_PICTURE.format(results[0][1], results[0][0]) - - return pic - - def delete_expired(self): - conn = sql.connect(self.file) - conn.execute(f'DELETE FROM mylist WHERE expires <= ?', (round(time()),),) - conn.commit() - conn.close() - - def flush(self): - conn = sql.connect(self.file) - conn.execute(f'DELETE FROM mylist') - conn.commit() - conn.close() \ No newline at end of file diff --git a/plugin.py b/plugin.py index 7a6cd0a..a928a0a 100644 --- a/plugin.py +++ b/plugin.py @@ -3,13 +3,13 @@ import xbmcgui import xbmcplugin -import tver +from lib import tver -from mylist import MyList -from watcher import Watcher -from cache import Cache -from favourites import show_favourites -from utils import log, show_info, check_if_kodi_supports_manifest, extract_info, extract_manifest_url_from_info, get_url, refresh, get_adaptive_type_from_url, localize, clear_thumbnails +from lib import Favourites +from lib import Watcher +from lib import MyList + +from lib import log, show_info, check_if_kodi_supports_manifest, extract_info, extract_manifest_url_from_info, get_url, refresh, get_adaptive_type_from_url, localize, clear_thumbnails _HANDLE = int(sys.argv[1]) @@ -27,7 +27,7 @@ def list_videos(category): context = (localize(30020),'delist') elif category == 'watching': - videos = tver.get_watching_episodes() + videos = Watcher().get_watching_episodes() else: videos = tver.get_episodes(category) @@ -135,7 +135,7 @@ def router(paramstring): elif action == 'thumbnails': clear_thumbnails() elif action == 'favourites': - show_favourites() + Favourites().list() else: raise ValueError('Invalid paramstring: {}!'.format(paramstring)) else: diff --git a/service.py b/service.py index 07b0ca1..06f70cb 100644 --- a/service.py +++ b/service.py @@ -1,11 +1,10 @@ -import tver -from cache import Cache -from mylist import MyList +from lib import Cache, tver, Favourites, MyList if __name__ == '__main__': #initialize DB mylist = MyList() Cache().create() + Favourites().create() mylist.create() # cache warming diff --git a/watcher.py b/watcher.py deleted file mode 100644 index 6d6d465..0000000 --- a/watcher.py +++ /dev/null @@ -1,63 +0,0 @@ -import sys - -from utils import lookup_db - -import sqlite3 as sql - -_URL = sys.argv[0] - -class Watcher: - def __init__(self): - self.filenameDb = lookup_db("MyVideos") - - def is_watching(self): - return bool( self.select() ) - - def select_watched_from_list(self, list): - if not list: - return [] - - watched = [] - - conn = sql.connect(self.filenameDb) - cur = conn.cursor() - - placeholders = ', '.join(['?'] * len(list)) - - cur.execute(''' - SELECT strFilename - FROM files - WHERE strFilename IN ({}) - AND playCount IS NOT NULL - '''.format(placeholders), list) - - results = cur.fetchall() - - watched = [result[0] for result in results] - - cur.close() - conn.close() - return watched - - def select(self): - conn = sql.connect(self.filenameDb) - cur = conn.cursor() - - stmt = ''' - SELECT f.strFilename, - b.timeInSeconds, - b.totalTimeInSeconds - FROM bookmark as b - INNER JOIN files as f ON f.idFile = b.idFile - INNER JOIN path as p ON p.idPath = f.idPath - WHERE f.playCount IS NULL - AND p.strPath = ? - ''' - cur.execute(stmt, (_URL,),) - - results = cur.fetchall() - - cur.close() - conn.close() - - return results \ No newline at end of file