Update code
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -160,3 +160,6 @@ cython_debug/
|
||||
#.idea/
|
||||
|
||||
config.yaml
|
||||
*.mo
|
||||
*.po~
|
||||
output/
|
||||
|
||||
1
compile_i18n.sh
Executable file
1
compile_i18n.sh
Executable file
@@ -0,0 +1 @@
|
||||
msgmerge --for-msgfmt jellyfinstats/language/zh_CN/LC_MESSAGES/jellyfinStats.po jellyfinstats/language/jellyfinStats.po | msgfmt -o jellyfinstats/language/zh_CN/LC_MESSAGES/jellyfinStats.mo -
|
||||
@@ -0,0 +1,10 @@
|
||||
from gettext import bindtextdomain, gettext as _, textdomain # noqa: F401
|
||||
from os.path import join
|
||||
|
||||
|
||||
try:
|
||||
bindtextdomain('jellyfinStats', join(__path__[0], 'language'))
|
||||
textdomain('jellyfinStats')
|
||||
except Exception:
|
||||
from traceback import print_exc
|
||||
print_exc()
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
from argparse import ArgumentParser
|
||||
from . import _
|
||||
from .audio import generate_audio_report
|
||||
from .cache import IdRelativeCache
|
||||
from .config import Config
|
||||
from .db import PlaybackReportingDb, LibraryDb
|
||||
|
||||
|
||||
p = ArgumentParser()
|
||||
p.add_argument("-c", "--config", help="The path to config file.", default="config.yaml") # noqa: E501
|
||||
p.add_argument("--playback-reporting-db", help="The path to playback_reporting.db") # noqa: E501
|
||||
p.add_argument("--library-db", help="The path to library.db")
|
||||
p.add_argument("--jellyfin-data-dir", help="The path to data directory.")
|
||||
p = ArgumentParser(prog="jellyfinstats")
|
||||
p.add_argument("-c", "--config", help=_("The path to config file."), default="config.yaml") # noqa: E501
|
||||
p.add_argument("--playback-reporting-db", help=_("The path to playback_reporting.db")) # noqa: E501
|
||||
p.add_argument("--library-db", help=_("The path to library.db"))
|
||||
p.add_argument("--jellyfin-data-dir", help=_("The path to jellyfin data directory.")) # noqa: E501
|
||||
p.add_argument("--output-dir", help=_("The directory for output files."))
|
||||
p.add_argument("--ask-page-size", help=_("Specify maximum items to display in one page."), type=int) # noqa: E501
|
||||
arg = p.parse_intermixed_args()
|
||||
cfg = Config(arg.config, arg)
|
||||
with PlaybackReportingDb(cfg.playback_reporting_db) as pdb:
|
||||
with LibraryDb(cfg.library_db) as ldb:
|
||||
generate_audio_report(pdb, ldb)
|
||||
with IdRelativeCache(cfg.output_dir) as icache:
|
||||
generate_audio_report(pdb, ldb, icache, cfg)
|
||||
|
||||
@@ -1,23 +1,152 @@
|
||||
from . import _
|
||||
from .cache import IdRelativeCache
|
||||
from .config import Config
|
||||
from .db import PlaybackReportingDb, LibraryDb
|
||||
from .utils import ask_choice
|
||||
from re import compile
|
||||
|
||||
|
||||
ITEMNAME_PATTERN = compile(r'(?P<album_artist>.*) - (?P<track>.*) \((?P<album>.*)\)') # noqa: E501
|
||||
|
||||
|
||||
def generate_audio_report(pdb: PlaybackReportingDb, ldb: LibraryDb):
|
||||
def print_item(item):
|
||||
s = item['Name']
|
||||
if item['Album']:
|
||||
s += "\n" + _("Album: ") + item['Album']
|
||||
if item['Artists']:
|
||||
s += "\n" + _("Artists: ") + item['Artists']
|
||||
if item['AlbumArtists']:
|
||||
s += "\n" + _("Album artists: ") + item['AlbumArtists']
|
||||
return s
|
||||
|
||||
|
||||
class AudioSelector:
|
||||
def __init__(self, cfg: Config, origin, ldb: LibraryDb, choices=None):
|
||||
self.cfg = cfg
|
||||
self.origin = origin
|
||||
self.ldb = ldb
|
||||
self.choices = choices if choices and len(choices) else None
|
||||
self.re = None
|
||||
self.fns = []
|
||||
|
||||
def print_original(self):
|
||||
print(_("Original item: ") + self.origin['track'])
|
||||
if self.origin['album']:
|
||||
print(_("Album: ") + self.origin['album'])
|
||||
if self.origin['album_artist']:
|
||||
print(_("Album artists: ") + self.origin['album_artist'])
|
||||
|
||||
def choose_in_choices(self):
|
||||
item = ask_choice(self.cfg, self.choices, _("Please choose audio item:"), print_item, (("x", _("No item"), "none"), ("o", _("Choose other items"), "other"),)) # noqa: E501
|
||||
self.re = item
|
||||
if item == "none":
|
||||
self.re = None
|
||||
elif item == "other":
|
||||
self.fns.append(self.choose_others)
|
||||
|
||||
def choose_others(self):
|
||||
act = [("i", _("Input item id"), "id"), ("t", _("Input track name"), "track"), ("a", _("Input album name"), "album"), ("x", _("No item"), "none")] # noqa: E501
|
||||
if self.choices:
|
||||
act.append(("o", _("Choose in given choices"), "choose"))
|
||||
re = ask_choice(self.cfg, [], _("Please choose action: "), extra=act)
|
||||
if re == "choose":
|
||||
self.fns.append(self.choose_in_choices)
|
||||
elif re == "id":
|
||||
self.fns.append(self.input_id)
|
||||
elif re == "track":
|
||||
self.fns.append(self.input_track)
|
||||
elif re == "album":
|
||||
self.fns.append(self.input_album)
|
||||
elif re == "none":
|
||||
self.re = None
|
||||
|
||||
def input_id(self):
|
||||
id = input(_("Please input item id:"))
|
||||
item = self.ldb.get_item(id)
|
||||
if item and item['type'] == 'MediaBrowser.Controller.Entities.Audio.Audio': # noqa: E501
|
||||
self.re = item
|
||||
else:
|
||||
print(_("Item not found."))
|
||||
self.fns.append(self.choose_others)
|
||||
|
||||
def handle_items(self, items):
|
||||
if len(items):
|
||||
act = [("b", _("Back"), "back"), ("x", _("No item"), "none")]
|
||||
if self.choices:
|
||||
act.append(("o", _("Choose in given choices"), "choose"))
|
||||
self.re = ask_choice(self.cfg, items, _("Please choose audio item:"), print_item, act) # noqa: E501
|
||||
if self.re == "back":
|
||||
self.fns.append(self.choose_others)
|
||||
elif self.re == "choose":
|
||||
self.fns.append(self.choose_in_choices)
|
||||
elif self.re == "none":
|
||||
self.re = None
|
||||
else:
|
||||
print(_("Items not found."))
|
||||
self.fns.append(self.choose_others)
|
||||
|
||||
def input_track(self):
|
||||
track = input(_("Please input track name:"))
|
||||
items = self.ldb.get_audios(track)
|
||||
self.handle_items(items)
|
||||
|
||||
def input_album(self):
|
||||
album = input(_("Please input album name:"))
|
||||
items = self.ldb.get_audios(album=album)
|
||||
self.handle_items(items)
|
||||
|
||||
def ask(self):
|
||||
self.print_original()
|
||||
if self.choices:
|
||||
self.fns.append(self.choose_in_choices)
|
||||
else:
|
||||
self.fns.append(self.choose_others)
|
||||
while True:
|
||||
if len(self.fns) == 0:
|
||||
break
|
||||
self.fns.pop(0)()
|
||||
return self.re
|
||||
|
||||
|
||||
def generate_audio_report(pdb: PlaybackReportingDb, ldb: LibraryDb,
|
||||
icache: IdRelativeCache, cfg: Config):
|
||||
offset = 0
|
||||
data = pdb.get_activitys(offset, itemType='Audio')
|
||||
count = 0
|
||||
re = None
|
||||
itemMap = {}
|
||||
itemList = {}
|
||||
while len(data) > 0:
|
||||
for d in data:
|
||||
itemId = d['ItemId']
|
||||
item = ldb.get_item(itemId)
|
||||
if item:
|
||||
pass
|
||||
if not item:
|
||||
re = icache.get(itemId)
|
||||
if re:
|
||||
item = ldb.get_item(re['id'])
|
||||
if item and isinstance(item, str):
|
||||
if item == 'no_track':
|
||||
if itemId in itemMap:
|
||||
itemList[itemId].append(d)
|
||||
else:
|
||||
itemMap[itemId] = d
|
||||
itemList[itemId] = [d]
|
||||
continue
|
||||
elif item and item['PresentationUniqueKey'] in itemMap:
|
||||
itemList[item['PresentationUniqueKey']].append(d)
|
||||
continue
|
||||
else:
|
||||
if itemId in itemMap:
|
||||
itemList[itemId].append(d)
|
||||
continue
|
||||
else:
|
||||
if itemId in itemMap:
|
||||
itemList[itemId].append(d)
|
||||
continue
|
||||
if not item:
|
||||
itemName = d['ItemName']
|
||||
re = ITEMNAME_PATTERN.match(itemName)
|
||||
if re is None:
|
||||
re = ITEMNAME_PATTERN.match(itemName)
|
||||
if re is None:
|
||||
raise ValueError(f"Failed to parse ItemName: {itemName}")
|
||||
re = re.groupdict()
|
||||
@@ -27,13 +156,29 @@ def generate_audio_report(pdb: PlaybackReportingDb, ldb: LibraryDb):
|
||||
re['album'] = None
|
||||
items = ldb.get_audios(re['track'], re['album'])
|
||||
if len(items) == 1:
|
||||
pass
|
||||
newId = items[0]['PresentationUniqueKey']
|
||||
icache.set(itemId, newId, {'album': items[0]['Album'], 'track': items[0]['Name'], 'album_artist': items[0]['AlbumArtists'], 'original': re}) # noqa: E501
|
||||
item = items[0]
|
||||
else:
|
||||
if len(items):
|
||||
print(items)
|
||||
if not len(items):
|
||||
items = ldb.get_audios(re['track'])
|
||||
if not len(items) and re['album']:
|
||||
items = ldb.get_audios(album=re['album'])
|
||||
item = AudioSelector(cfg, re, ldb, items).ask()
|
||||
if item:
|
||||
newId = item['PresentationUniqueKey']
|
||||
icache.set(itemId, newId, {'album': item['Album'], 'track': item['Name'], 'album_artist': item['AlbumArtists'], 'original': re}) # noqa: E501
|
||||
else:
|
||||
print(re)
|
||||
count += 1
|
||||
icache.set_value(itemId, 'no_track')
|
||||
if item:
|
||||
itemId = item['PresentationUniqueKey']
|
||||
itemMap[itemId] = item
|
||||
itemList[itemId] = [d]
|
||||
else:
|
||||
print(d)
|
||||
itemMap[itemId] = d
|
||||
itemList[itemId] = [d]
|
||||
count += 1
|
||||
offset += len(data)
|
||||
data = pdb.get_activitys(offset, itemType='Audio')
|
||||
print('Count', count)
|
||||
|
||||
54
jellyfinstats/cache.py
Normal file
54
jellyfinstats/cache.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from os import makedirs
|
||||
from os.path import exists, join
|
||||
from typing import Dict, Any
|
||||
from yaml import dump as dumpyaml, load as loadyaml
|
||||
try:
|
||||
from yaml import CSafeDumper as SafeDumper, CSafeLoader as SafeLoader
|
||||
except ImportError:
|
||||
from yaml import SafeDumper, SafeLoader
|
||||
from . import _
|
||||
|
||||
|
||||
class IdRelativeCache:
|
||||
def __init__(self, output_dir: str):
|
||||
makedirs(output_dir, exist_ok=True)
|
||||
self._path = join(output_dir, 'id_relative_cache.yaml')
|
||||
self._data = {}
|
||||
if exists(self._path):
|
||||
try:
|
||||
with open(self._path, "r", encoding="UTF-8") as f:
|
||||
data = loadyaml(f, SafeLoader)
|
||||
version = data['version']
|
||||
if version > 1:
|
||||
t = _("Unsupported version: ")
|
||||
raise NotImplementedError(f'{t}{version}')
|
||||
self._data = data['data']
|
||||
except Exception:
|
||||
from traceback import print_exc
|
||||
print_exc()
|
||||
print(_("Failed to load cache."))
|
||||
self._closed = False
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, tp, val, trace):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
if self._closed:
|
||||
return
|
||||
with open(self._path, "w", encoding='UTF-8') as f:
|
||||
dumpyaml({'version': 1, 'data': self._data}, f, SafeDumper,
|
||||
allow_unicode=True)
|
||||
self._closed = True
|
||||
|
||||
def get(self, oldId: str):
|
||||
return self._data[oldId] if oldId in self._data else None
|
||||
|
||||
def set(self, oldId: str, newId: str, data: Dict[str, Any]):
|
||||
self._data[oldId] = {k: data[k] for k in data}
|
||||
self._data[oldId]['id'] = newId
|
||||
|
||||
def set_value(self, oldId: str, value):
|
||||
self._data[oldId] = value
|
||||
@@ -9,14 +9,25 @@ try:
|
||||
from yaml import CSafeLoader as SafeLoader
|
||||
except ImportError:
|
||||
from yaml import SafeLoader
|
||||
from . import _
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, path: str, args: Namespace = None):
|
||||
with open(path, encoding="UTF-8") as f:
|
||||
self._data = loadyaml(f, Loader=SafeLoader)
|
||||
if self._data is None:
|
||||
self._data = {}
|
||||
self._args = args
|
||||
|
||||
@cached_property
|
||||
def ask_page_size(self) -> int:
|
||||
if self._args and self._args.ask_page_size is not None:
|
||||
return self._args.ask_page_size
|
||||
if 'ask_page_size' in self._data and isinstance(self._data['ask_page_size'], int): # noqa: E501
|
||||
return self._data['ask_page_size']
|
||||
return 10
|
||||
|
||||
@cached_property
|
||||
def playback_reporting_db(self) -> str:
|
||||
if self._args and self._args.playback_reporting_db:
|
||||
@@ -26,7 +37,7 @@ class Config:
|
||||
d = self.jellyfin_data_dir
|
||||
if d:
|
||||
return join(d, "playback_reporting.db")
|
||||
raise ValueError('playback_reporting.db not set.')
|
||||
raise ValueError(_('%s not set.') % ('playback_reporting_db'))
|
||||
|
||||
@cached_property
|
||||
def library_db(self) -> str:
|
||||
@@ -37,7 +48,7 @@ class Config:
|
||||
d = self.jellyfin_data_dir
|
||||
if d:
|
||||
return join(d, "library.db")
|
||||
raise ValueError('library.db not set.')
|
||||
raise ValueError(_('%s not set.') % ('library_db'))
|
||||
|
||||
@cached_property
|
||||
def jellyfin_data_dir(self) -> str | None:
|
||||
@@ -45,3 +56,11 @@ class Config:
|
||||
return self._args.jellyfin_data_dir
|
||||
if 'jellyfin_data_dir' in self._data and self._data['jellyfin_data_dir']: # noqa: E501
|
||||
return self._data['jellyfin_data_dir']
|
||||
|
||||
@cached_property
|
||||
def output_dir(self) -> str:
|
||||
if self._args and self._args.output_dir:
|
||||
return self._args.output_dir
|
||||
if 'output_dir' in self._data and self._data['output_dir']:
|
||||
return self._data['output_dir']
|
||||
return 'output'
|
||||
|
||||
@@ -60,12 +60,15 @@ class LibraryDb:
|
||||
re = cur.fetchone()
|
||||
return dict(re) if re is not None else None
|
||||
|
||||
def get_audios(self, track: str, album: str = None):
|
||||
args = ['Audio', track]
|
||||
def get_audios(self, track: str = None, album: str = None):
|
||||
args = ['MediaBrowser.Controller.Entities.Audio.Audio']
|
||||
where_sql = ''
|
||||
if track is not None:
|
||||
where_sql += ' AND Name = ?'
|
||||
args.append(track)
|
||||
if album is not None:
|
||||
where_sql = ' AND Album = ?'
|
||||
where_sql += ' AND Album = ?'
|
||||
args.append(album)
|
||||
cur = self._db.execute(f"SELECT * FROM TypedBaseItems WHERE MediaType = ? AND Name = ?{where_sql};", args) # noqa: E501
|
||||
cur = self._db.execute(f"SELECT * FROM TypedBaseItems WHERE type = ?{where_sql};", args) # noqa: E501
|
||||
cur.row_factory = sqlite3.Row
|
||||
return [dict(i) for i in cur.fetchall()]
|
||||
|
||||
152
jellyfinstats/language/jellyfinStats.po
Normal file
152
jellyfinstats/language/jellyfinStats.po
Normal file
@@ -0,0 +1,152 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR lifegpc
|
||||
# This file is distributed under the same license as the jellyfinStats package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: jellyfinStats 1.0\n"
|
||||
"Report-Msgid-Bugs-To: [email protected]\n"
|
||||
"POT-Creation-Date: 2024-05-15 13:56+0800\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <[email protected]>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: __main__.py:10
|
||||
msgid "The path to config file."
|
||||
msgstr ""
|
||||
|
||||
#: __main__.py:11
|
||||
msgid "The path to playback_reporting.db"
|
||||
msgstr ""
|
||||
|
||||
#: __main__.py:12
|
||||
msgid "The path to library.db"
|
||||
msgstr ""
|
||||
|
||||
#: __main__.py:13
|
||||
msgid "The path to jellyfin data directory."
|
||||
msgstr ""
|
||||
|
||||
#: __main__.py:14
|
||||
msgid "The directory for output files."
|
||||
msgstr ""
|
||||
|
||||
#: __main__.py:15
|
||||
msgid "Specify maximum items to display in one page."
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:15 audio.py:35
|
||||
msgid "Album: "
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:17
|
||||
msgid "Artists: "
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:19 audio.py:37
|
||||
msgid "Album artists: "
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:33
|
||||
msgid "Original item: "
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:40 audio.py:77
|
||||
msgid "Please choose audio item:"
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:40 audio.py:48 audio.py:74
|
||||
msgid "No item"
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:40
|
||||
msgid "Choose other items"
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:48
|
||||
msgid "Input item id"
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:48
|
||||
msgid "Input track name"
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:48
|
||||
msgid "Input album name"
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:50 audio.py:76
|
||||
msgid "Choose in given choices"
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:51
|
||||
msgid "Please choose action: "
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:64
|
||||
msgid "Please input item id:"
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:69
|
||||
msgid "Item not found."
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:74
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:85
|
||||
msgid "Items not found."
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:89
|
||||
msgid "Please input track name:"
|
||||
msgstr ""
|
||||
|
||||
#: audio.py:94
|
||||
msgid "Please input album name:"
|
||||
msgstr ""
|
||||
|
||||
#: cache.py:23
|
||||
msgid "Unsupported version: "
|
||||
msgstr ""
|
||||
|
||||
#: cache.py:29
|
||||
msgid "Failed to load cache."
|
||||
msgstr ""
|
||||
|
||||
#: config.py:40 config.py:51
|
||||
#, python-format
|
||||
msgid "%s not set."
|
||||
msgstr ""
|
||||
|
||||
#: utils.py:6
|
||||
msgid "Please choose: "
|
||||
msgstr ""
|
||||
|
||||
#: utils.py:23
|
||||
#, python-format
|
||||
msgid "Page %i/%i"
|
||||
msgstr ""
|
||||
|
||||
#: utils.py:31
|
||||
msgid "First page"
|
||||
msgstr ""
|
||||
|
||||
#: utils.py:33
|
||||
msgid "Previous page"
|
||||
msgstr ""
|
||||
|
||||
#: utils.py:36
|
||||
msgid "Next page"
|
||||
msgstr ""
|
||||
|
||||
#: utils.py:38
|
||||
msgid "Last page"
|
||||
msgstr ""
|
||||
151
jellyfinstats/language/zh_CN/LC_MESSAGES/jellyfinStats.po
Normal file
151
jellyfinstats/language/zh_CN/LC_MESSAGES/jellyfinStats.po
Normal file
@@ -0,0 +1,151 @@
|
||||
# Chinese translations for jellyfinStats package.
|
||||
# Copyright (C) 2024 lifegpc
|
||||
# This file is distributed under the same license as the jellyfinStats package.
|
||||
# mhy <[email protected]>, 2024.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: jellyfinStats 1.0\n"
|
||||
"Report-Msgid-Bugs-To: [email protected]\n"
|
||||
"POT-Creation-Date: 2024-05-15 13:56+0800\n"
|
||||
"PO-Revision-Date: 2024-05-15 10:20+0800\n"
|
||||
"Last-Translator: mhy <[email protected]>\n"
|
||||
"Language-Team: Chinese (simplified) <[email protected]>\n"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: __main__.py:10
|
||||
msgid "The path to config file."
|
||||
msgstr "配置文件位置。"
|
||||
|
||||
#: __main__.py:11
|
||||
msgid "The path to playback_reporting.db"
|
||||
msgstr "playback_reporting.db 文件位置。"
|
||||
|
||||
#: __main__.py:12
|
||||
msgid "The path to library.db"
|
||||
msgstr "library.db 文件位置。"
|
||||
|
||||
#: __main__.py:13
|
||||
msgid "The path to jellyfin data directory."
|
||||
msgstr "Jellyfin 数据目录位置。"
|
||||
|
||||
#: __main__.py:14
|
||||
msgid "The directory for output files."
|
||||
msgstr "输出目录位置。"
|
||||
|
||||
#: __main__.py:15
|
||||
msgid "Specify maximum items to display in one page."
|
||||
msgstr "指定一页可以显示的最大条目数。"
|
||||
|
||||
#: audio.py:15 audio.py:35
|
||||
msgid "Album: "
|
||||
msgstr "专辑:"
|
||||
|
||||
#: audio.py:17
|
||||
msgid "Artists: "
|
||||
msgstr "艺术家:"
|
||||
|
||||
#: audio.py:19 audio.py:37
|
||||
msgid "Album artists: "
|
||||
msgstr "专辑艺术家:"
|
||||
|
||||
#: audio.py:33
|
||||
msgid "Original item: "
|
||||
msgstr "原项目:"
|
||||
|
||||
#: audio.py:40 audio.py:77
|
||||
msgid "Please choose audio item:"
|
||||
msgstr "请选择音乐项目:"
|
||||
|
||||
#: audio.py:40 audio.py:48 audio.py:74
|
||||
msgid "No item"
|
||||
msgstr "无对应项目"
|
||||
|
||||
#: audio.py:40
|
||||
msgid "Choose other items"
|
||||
msgstr "选择其他项目"
|
||||
|
||||
#: audio.py:48
|
||||
msgid "Input item id"
|
||||
msgstr "输入项目ID"
|
||||
|
||||
#: audio.py:48
|
||||
msgid "Input track name"
|
||||
msgstr "输入标题"
|
||||
|
||||
#: audio.py:48
|
||||
msgid "Input album name"
|
||||
msgstr "输入专辑名称"
|
||||
|
||||
#: audio.py:50 audio.py:76
|
||||
msgid "Choose in given choices"
|
||||
msgstr "从给出的选项中选择"
|
||||
|
||||
#: audio.py:51
|
||||
msgid "Please choose action: "
|
||||
msgstr "请选择方式:"
|
||||
|
||||
#: audio.py:64
|
||||
msgid "Please input item id:"
|
||||
msgstr "请输入项目ID:"
|
||||
|
||||
#: audio.py:69
|
||||
msgid "Item not found."
|
||||
msgstr "没有找到项目。"
|
||||
|
||||
#: audio.py:74
|
||||
msgid "Back"
|
||||
msgstr "返回"
|
||||
|
||||
#: audio.py:85
|
||||
msgid "Items not found."
|
||||
msgstr "没有找到项目。"
|
||||
|
||||
#: audio.py:89
|
||||
msgid "Please input track name:"
|
||||
msgstr "请输入标题:"
|
||||
|
||||
#: audio.py:94
|
||||
msgid "Please input album name:"
|
||||
msgstr "请输入专辑名称:"
|
||||
|
||||
#: cache.py:23
|
||||
msgid "Unsupported version: "
|
||||
msgstr "不支持的版本:"
|
||||
|
||||
#: cache.py:29
|
||||
msgid "Failed to load cache."
|
||||
msgstr "加载缓存失败。"
|
||||
|
||||
#: config.py:40 config.py:51
|
||||
#, python-format
|
||||
msgid "%s not set."
|
||||
msgstr "%s 未设置。"
|
||||
|
||||
#: utils.py:6
|
||||
msgid "Please choose: "
|
||||
msgstr "请选择:"
|
||||
|
||||
#: utils.py:23
|
||||
#, python-format
|
||||
msgid "Page %i/%i"
|
||||
msgstr "第%i/%i页"
|
||||
|
||||
#: utils.py:31
|
||||
msgid "First page"
|
||||
msgstr "第一页"
|
||||
|
||||
#: utils.py:33
|
||||
msgid "Previous page"
|
||||
msgstr "上一页"
|
||||
|
||||
#: utils.py:36
|
||||
msgid "Next page"
|
||||
msgstr "下一页"
|
||||
|
||||
#: utils.py:38
|
||||
msgid "Last page"
|
||||
msgstr "最后一页"
|
||||
68
jellyfinstats/utils.py
Normal file
68
jellyfinstats/utils.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from math import ceil
|
||||
from . import _
|
||||
from .config import Config
|
||||
|
||||
|
||||
def ask_choice(cfg: Config, choices: list, prompt=_("Please choose: "),
|
||||
fn=None, extra=None):
|
||||
if extra:
|
||||
for n in extra:
|
||||
if n[0] in ['f', 'p', 'n', 'l']:
|
||||
raise ValueError(f'Internal action used: {n[0]}')
|
||||
page_size = cfg.ask_page_size
|
||||
if page_size <= 0:
|
||||
page_size = 10
|
||||
count = len(choices)
|
||||
total_pages = ceil(count / page_size)
|
||||
page = 1
|
||||
|
||||
def show_page():
|
||||
nonlocal page
|
||||
base = (page - 1) * page_size
|
||||
if total_pages > 1:
|
||||
print(_("Page %i/%i") % (page, total_pages))
|
||||
for i in range(page_size):
|
||||
index = base + i
|
||||
if index >= count:
|
||||
break
|
||||
s = fn(choices[index]) if fn else choices[index]
|
||||
print(f"{i}. {s}")
|
||||
if page > 1:
|
||||
fp = _("First page")
|
||||
print(f'f. {fp}')
|
||||
pp = _("Previous page")
|
||||
print(f'p. {pp}')
|
||||
if page < total_pages:
|
||||
np = _("Next page")
|
||||
print(f'n. {np}')
|
||||
lp = _("Last page")
|
||||
print(f'l. {lp}')
|
||||
if extra is not None:
|
||||
for t in extra:
|
||||
print(f"{t[0]}. {t[1]}")
|
||||
|
||||
while True:
|
||||
show_page()
|
||||
s = input(prompt)
|
||||
if s == "f":
|
||||
page = 1
|
||||
elif s == "p":
|
||||
page = max(1, page - 1)
|
||||
elif s == "n":
|
||||
page = min(total_pages, page + 1)
|
||||
elif s == "l":
|
||||
page = total_pages
|
||||
else:
|
||||
if extra is not None:
|
||||
for t in extra:
|
||||
if t[0] == s:
|
||||
return t[2]
|
||||
try:
|
||||
index = int(s)
|
||||
except Exception:
|
||||
continue
|
||||
base = (page - 1) * page_size
|
||||
index += base
|
||||
if index < 0 or index >= count:
|
||||
continue
|
||||
return choices[index]
|
||||
4
update_i18n.sh
Executable file
4
update_i18n.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
xgettext -D jellyfinstats __main__.py audio.py cache.py config.py utils.py \
|
||||
-L python -d jellyfinStats -p jellyfinstats/language --copyright-holder 'lifegpc' --package-name 'jellyfinStats' --package-version '1.0' --msgid-bugs-address '[email protected]'
|
||||
sed -i 's/charset=CHARSET/charset=UTF-8/g' jellyfinstats/language/jellyfinStats.po
|
||||
msgmerge -U jellyfinstats/language/zh_CN/LC_MESSAGES/jellyfinStats.po jellyfinstats/language/jellyfinStats.po
|
||||
Reference in New Issue
Block a user