Compare commits

...

5 Commits

Author SHA1 Message Date
restitux 6ace97ff83 Add image caching logic for show posters 2022-09-13 22:57:43 -06:00
restitux 9234da67fe Remove unused Python code 2022-09-13 22:17:42 -06:00
restitux 3227730a37 Add config file for specifying backends 2022-09-13 22:16:51 -06:00
restitux afb7a456a1 Cleanup commented out lines 2022-09-13 21:36:57 -06:00
restitux a38221cca2 Add episode browser 2022-09-13 21:33:26 -06:00
12 changed files with 343 additions and 70 deletions
+123 -50
View File
@@ -1,30 +1,54 @@
from typing import Type
import os
import shutil
import sys
from urllib import parse
import urllib.parse
from collections import defaultdict
from PyQt5.QtWidgets import QApplication
from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine
from PyQt5.QtQuick import QQuickImageProvider
# from PyQt5.QtQuick import QQuickView
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSlot
from PyQt5.QtGui import QImage
# from PyQt5.QtCore import QObject, QUrl, pyqtProperty
from .qtmpv import MpvObject
import requests
import toml
class Show(QObject):
class Episode(QObject):
def __init__(self, source, parent=None):
super().__init__(parent)
self._source = source
@pyqtProperty("QString", constant=True)
def title(self):
return self._source["Title"]
@pyqtProperty(int, constant=True)
def season(self):
return self._source["Season"]
@pyqtProperty(int, constant=True)
def episode(self):
return self._source["Episode"]
@pyqtProperty("QString", constant=True)
def filename(self):
return self._source["OriginalFilename"]
class Show(QObject):
def __init__(self, source, episodes, parent=None):
super().__init__(parent)
self._source = source
self._episodes = episodes
@pyqtProperty("QString", constant=True)
def title(self) -> str:
return self._source["title"]
@@ -37,10 +61,6 @@ class Show(QObject):
def description(self) -> str:
return self._source["description"]
@pyqtProperty(int, constant=True)
def episodes(self) -> int:
return self._source["episodes"]
@pyqtProperty(int, constant=True)
def watched(self) -> int:
return self._source["watched"]
@@ -49,6 +69,10 @@ class Show(QObject):
def poster(self) -> str:
return self._source["poster"]
@pyqtProperty(list, constant=True)
def episodes(self) -> list[Episode]:
return self._episodes
class ProviderImageProvider(QQuickImageProvider):
def __init__(self, icon_data):
@@ -60,26 +84,21 @@ class ProviderImageProvider(QQuickImageProvider):
print(p_str)
print(size)
# img = QImage.fromData(self._icon_data.encode("utf-8"))
import base64
data: bytes = base64.b64decode(self._icon_data)
img = QImage.fromData(data)
# img = QImage.fromData(self._icon_data.encode("utf-8"))
# img = QImage(300, 300, QImage.Format_RGBA8888)
# img.fill(Qt.red)
return img, img.size()
def getUrl(base: str, path: str) -> dict:
url: str = parse.urljoin(base, path)
url: str = urllib.parse.urljoin(base, path)
r: requests.Response = requests.get(url)
return r.json()
class Provider(QObject):
def __init__(self, url: str, parent=None):
def __init__(self, url: str, data_dir: str, parent=None):
super().__init__(parent)
self.url: str = url
@@ -89,8 +108,47 @@ class Provider(QObject):
describe["icon"]
)
# Create dictionary of show episodes
episodes: dict = getUrl(self.url, "episodes")
_episodes: defaultdict[str, list[Episode]] = defaultdict(lambda: [])
for e in episodes["episodes"]:
_episodes[e["ShowTitle"]].append(Episode(e))
shows: dict = getUrl(self.url, "shows")
self._shows: dict[int, Show] = {e["id"]: Show(e) for e in shows["data"]}
# Create image_cache directory
provider_data_dir: str = os.path.join(
data_dir, "image_cache", "providers", self._name
)
os.makedirs(provider_data_dir, exist_ok=True)
# Cache show posters
for s in shows["data"]:
# Get local cache path
poster_url: str = s["poster"]
poster_path: str = urllib.parse.urlparse(poster_url).path
poster_ext: str = os.path.splitext(poster_path)[1]
download_path: str = os.path.join(
provider_data_dir, str(s["id"]) + poster_ext
)
# Download poster
try:
with open(download_path, "xb") as f:
r = requests.get(poster_url, stream=True)
if r.status_code == 200:
r.raw.decode_content = True
shutil.copyfileobj(r.raw, f)
except FileExistsError:
pass
# Overwrite poster URL with local path
s["poster"] = download_path
self._shows: dict[int, Show] = {
e["id"]: Show(e, _episodes[e["title"]]) for e in shows["data"]
}
recently_added: dict = getUrl(self.url, "recently_added")
self._recently_added: list[int] = recently_added["data"]
@@ -106,7 +164,6 @@ class Provider(QObject):
def logo(self) -> str:
return f"image://{self._name}/logo"
# @pyqtProperty("QObject")
@pyqtSlot(int, result=QObject)
def getShow(self, id) -> Show:
return self._shows[id]
@@ -131,8 +188,8 @@ class Provider(QObject):
class DataSource:
def __init__(self, providers=[]):
self.providers: list[Provider] = [Provider(url) for url in providers]
def __init__(self, providers: list[str], data_dir: str):
self.providers: list[Provider] = [Provider(url, data_dir) for url in providers]
def DatabaseType(data_source) -> Type:
@@ -149,6 +206,42 @@ def DatabaseType(data_source) -> Type:
return Database
def load_config() -> list[str]:
try:
config_dir: str = os.path.join(os.environ["XDG_CONFIG_HOME"], "ikinuki")
except:
config_dir: str = os.path.join(os.environ["HOME"], ".config", "ikinuki")
os.makedirs(config_dir, exist_ok=True)
config_file: str = os.path.join(config_dir, "client.toml")
try:
config: dict = toml.load(config_file)
except FileNotFoundError:
print(f'Config file not found at "{config_file}"')
print("Writing example config file. Please update and relaunch.")
default_config = """# [[backends]]
# address = "127.0.0.1"
# port = 32520"""
with open(config_file, "w") as f:
f.write(default_config)
sys.exit(-1)
return [f'http://{b["address"]}:{b["port"]}/' for b in config["backends"]]
def get_data_dir() -> str:
try:
data_dir: str = os.path.join(os.environ["XDG_DATA_HOME"], "ikinuki")
except:
data_dir: str = os.path.join(os.environ["HOME"], ".local", "share", "ikinuki")
os.makedirs(data_dir, exist_ok=True)
return data_dir
def main():
app = QApplication(sys.argv)
@@ -157,16 +250,17 @@ def main():
locale.setlocale(locale.LC_NUMERIC, "C")
data_source = DataSource(
[
"http://127.0.0.1:8080/a/",
"http://127.0.0.1:8080/b/",
"http://127.0.0.1:8080/c/",
]
)
try:
backends: list[str] = load_config()
except Exception as e:
print(f"ERROR: Could not load config file: {repr(e)}")
sys.exit(-1)
data_dir: str = get_data_dir()
data_source = DataSource(backends, data_dir)
qmlRegisterType(DatabaseType(data_source), "Ikinuki.Client", 1, 0, "Database")
# qmlRegisterType(Provider, "Ikinuki.Client", 1, 0, "Provider")
engine = QQmlApplicationEngine()
for provider in data_source.providers:
@@ -182,24 +276,3 @@ def main():
win.show()
sys.exit(app.exec_())
#
#
#
# app = QApplication([])
#
#
# window = QQmlApplicationEngine("layouts/mpv.qml")
# window.run
#
# view = QQuickView()
# url = QUrl("layouts/mpv.qml")
#
# import locale
#
# locale.setlocale(locale.LC_NUMERIC, 'C')
#
# view.setSource(url)
# view.show()
# app.exec_()
+1 -1
View File
@@ -18,7 +18,7 @@ Row {
ContentView {
id: view
viewSelected: selectedView == 1
currentIndex: selectedProvider + (browse ? db.Providers.length : 0)
parentIndex: selectedProvider + (browse ? db.Providers.length : 0)
providers: db.Providers
}
function mod(n, m) {
+35 -7
View File
@@ -11,8 +11,14 @@ StackLayout {
id: tabView
property var providers: []
property bool viewSelected
state: viewSelected ? "selected" : "deselected"
property int parentIndex
property int ySelect: 0
property bool showViewActive: false
currentIndex: showViewActive ? tabView.children.length - 3 : parentIndex
state: viewSelected ? "selected" : "deselected"
width: parent.width * viewSelected ? 0.95 : 0.8
height: parent.height
@@ -28,6 +34,10 @@ StackLayout {
provider: modelData
}
}
ShowView {
id: showView
show: providers[0].getShow(providers[0].showsAlphabetic[0])
}
states: [
State {
name: "deselected"
@@ -54,14 +64,32 @@ StackLayout {
}
]
function getCurrentIndex() {
if (showViewActive) {
var x = tabView.children.length - 1;
} else {
var x = currentIndex > (providers.length - 1) ? currentIndex + 1 : currentIndex;
}
return x;
}
Keys.onPressed: (event)=> {
var x = currentIndex > (providers.length - 1) ? currentIndex + 1 : currentIndex;
//tabView.children[currentIndex].Keys.pressed(event);
var x = getCurrentIndex()
tabView.children[x].Keys.pressed(event);
if (tabView.children[x].viewExit) {
tabView.children[x].viewExit = false;
parent.browse = false;
parent.selectedView = 0;
if (tabView.children[x].enterShow) {
showViewActive = true;
showView.show = tabView.children[x].enterShowShow;
} else if (tabView.children[x].viewExit) {
if (showViewActive) {
showViewActive = false;
tabView.children[x].xIndex = 0;
tabView.children[x].viewExit = false;
tabView.children[getCurrentIndex()].enterShow = false;
} else {
tabView.children[x].viewExit = false;
parent.browse = false;
parent.selectedView = 0;
}
}
event.accepted = true;
}
@@ -0,0 +1,34 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 1.4
import QtQuick.Controls 2.15
import Ikinuki.Client 1.0
import "./ShowView"
ScrollView {
id: root
property var episodeModel
property int scrollIndex: 0
contentHeight: root.height * 0.1 * episodeModel.length
ScrollBar.vertical.position: scrollIndex / (episodeModel.length)
clip: true
Behavior on ScrollBar.vertical.position {
NumberAnimation {
duration: 200
easing.type: Easing.Linear
}
}
Column {
Repeater {
model: episodeModel
Episode {
episode: modelData
selected: index == xIndex
height: root.height * 0.1
width: root.width
}
}
}
}
@@ -11,6 +11,8 @@ Rectangle {
id: root
property var provider
property bool viewExit: false
property bool enterShow: false
property var enterShowShow
property int currentView: 0
color: "#22282A"
Row {
@@ -76,6 +78,9 @@ Rectangle {
coverGrid.scrollIndex--;
}
}
} else if (event.key == Qt.Key_Return) {
enterShowShow = root.provider.getShow(root.provider.showsAlphabetic[(coverGrid.xIndex + coverGrid.yIndex * coverGrid.numColumns)])
enterShow = true;
}
} else if (currentView == 1) { // alphabet
if (event.key == Qt.Key_Left) {
@@ -11,8 +11,9 @@ Rectangle {
id: root
property var provider
property int ySelect: 0
property int xSelect: 0
property bool viewExit: false
property bool enterShow: false
property var enterShowShow
color: "#22282A"
Row {
Item {
@@ -82,6 +83,9 @@ Rectangle {
ySelect++;
} else if (event.key == Qt.Key_Up) {
ySelect--;
} else if (event.key == Qt.Key_Return) {
enterShowShow = provider.getShow(elementColumn.children[ySelect].showId)
enterShow = true;
}
event.accepted = true;
}
@@ -0,0 +1,76 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 1.4
import QtQuick.Controls 2.15
import Ikinuki.Client 1.0
import "./ShowView"
Rectangle {
property var show
property bool viewExit: false
property bool enterShow: false
property int xIndex: 0
color: "#22282A"
Column {
anchors.fill: parent
anchors.leftMargin: parent.width * 0.01
anchors.rightMargin: parent.width * 0.01
anchors.topMargin: parent.height * 0.05
anchors.bottomMargin: parent.height * 0.01
Text { // header
height: parent.height * 0.1
width: parent.width
text: show.title
font.pointSize: 20
color: "#cdd7d9"
}
Row { // main view
height: parent.height * 0.8
width: parent.width
spacing: parent.width * 0.01
Item {
height: parent.height
width: height * 0.68
Image {
source: show.poster
anchors.fill: parent
mipmap: true
}
}
EpisodeView {
id: episodeView
height: parent.height
width: parent.width * 0.65
episodeModel: show.episodes
}
}
Item {
height: parent.height * 0.1
width: parent.width
}
}
Keys.onPressed: (event)=> {
if (event.key == Qt.Key_Up) {
if (xIndex > 0) {
xIndex--;
if (episodeView.scrollIndex > xIndex) {
episodeView.scrollIndex--;
}
}
} else if (event.key == Qt.Key_Down) {
if (xIndex < show.episodes.length - 1) {
if (episodeView.scrollIndex < (xIndex - 8)) {
episodeView.scrollIndex++;
}
xIndex++;
}
} else if (event.key == Qt.Key_Escape) {
viewExit = true;
}
event.accepted = true;
}
}
@@ -0,0 +1,49 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 1.4
import QtQuick.Controls 2.15
import Ikinuki.Client 1.0
Item {
property var episode
property bool selected
Rectangle {
anchors.fill: parent
color: "white"
visible: selected
radius: 10
}
Row {
anchors.fill: parent
Item {
height: parent.height
width: parent.width * 0.04
}
Column {
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 4
Text {
text: {
if (modelData.season == 0) {
return "S" + modelData.episode + " " + modelData.title
} else {
return String(modelData.episode).padStart(2, "0") + ". " + modelData.title
}
}
font.pointSize: 20
color: selected ? "#3c3c3c" : "#99afb4"
}
Item {
height: parent.height * 0.1
width: parent.width
}
Text {
text: " air date"
font.pointSize: 12
color: selected ? "#3c3c3c" : "#99afb4"
}
}
}
}
@@ -12,7 +12,6 @@ Item {
DropShadow {
anchors.fill: selector
verticalOffset: 5
//horizontalOffset: 5
samples: 20
color: "black"
opacity: 0.5
@@ -24,7 +23,6 @@ Item {
color: "white"
height: parent.height
width: parent.width * 0.95
//anchors.fill: parent
visible: selected && maximized
radius: 10
}
@@ -42,11 +40,6 @@ Item {
anchors.fill: parent
id: logo
source: provider.logo
//y: parent.y + (parent.height / 2) - height / 2
//anchors.top: parent.verticalCenter
// anchors.verticalCenter: text.verticalCenter
//sourceSize.height: 50
//sourceSize.width: 50
}
ColorOverlay {
anchors.fill: logo
@@ -66,8 +59,6 @@ Item {
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 4
visible: maximized
//verticalAlignment: Text.AlignVCenter
//height: parent.height
}
}
}
Generated
+14 -2
View File
@@ -62,7 +62,7 @@ optional = false
python-versions = ">=3.5"
[package.extras]
screenshot_raw = ["pillow"]
screenshot_raw = ["Pillow"]
[[package]]
name = "requests"
@@ -82,6 +82,14 @@ urllib3 = ">=1.21.1,<1.27"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "urllib3"
version = "1.26.12"
@@ -98,7 +106,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "7e77956c19bb0148f37be8283857fbd29805184149d6f2c1027daab624c66b80"
content-hash = "ab4b6be20253adf487e007f81b07f9f585bd4a61d4e51889f9356dfc0e03e206"
[metadata.files]
certifi = [
@@ -152,6 +160,10 @@ requests = [
{file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
{file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
urllib3 = [
{file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
{file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
+1
View File
@@ -10,6 +10,7 @@ python = "^3.10"
python-mpv = "^0.5.2"
PyQt5 = "^5.15.1"
requests = "^2.28.1"
toml = "^0.10.2"
[tool.poetry.scripts]