From df84f07bce8bdbd2f06f2b1fda853eb8772dd2fc Mon Sep 17 00:00:00 2001 From: lifegpc Date: Wed, 15 May 2024 14:03:28 +0800 Subject: [PATCH] Update code --- .gitignore | 3 + compile_i18n.sh | 1 + jellyfinstats/__init__.py | 10 ++ jellyfinstats/__main__.py | 17 +- jellyfinstats/audio.py | 163 +++++++++++++++++- jellyfinstats/cache.py | 54 ++++++ jellyfinstats/config.py | 23 ++- jellyfinstats/db.py | 11 +- jellyfinstats/language/jellyfinStats.po | 152 ++++++++++++++++ .../zh_CN/LC_MESSAGES/jellyfinStats.po | 151 ++++++++++++++++ jellyfinstats/utils.py | 68 ++++++++ update_i18n.sh | 4 + 12 files changed, 636 insertions(+), 21 deletions(-) create mode 100755 compile_i18n.sh create mode 100644 jellyfinstats/cache.py create mode 100644 jellyfinstats/language/jellyfinStats.po create mode 100644 jellyfinstats/language/zh_CN/LC_MESSAGES/jellyfinStats.po create mode 100644 jellyfinstats/utils.py create mode 100755 update_i18n.sh diff --git a/.gitignore b/.gitignore index 5e24060..e5375af 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,6 @@ cython_debug/ #.idea/ config.yaml +*.mo +*.po~ +output/ diff --git a/compile_i18n.sh b/compile_i18n.sh new file mode 100755 index 0000000..4138f69 --- /dev/null +++ b/compile_i18n.sh @@ -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 - diff --git a/jellyfinstats/__init__.py b/jellyfinstats/__init__.py index e69de29..f63d1ae 100644 --- a/jellyfinstats/__init__.py +++ b/jellyfinstats/__init__.py @@ -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() diff --git a/jellyfinstats/__main__.py b/jellyfinstats/__main__.py index d932c61..86f9009 100644 --- a/jellyfinstats/__main__.py +++ b/jellyfinstats/__main__.py @@ -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) diff --git a/jellyfinstats/audio.py b/jellyfinstats/audio.py index 17a0eaa..6fee894 100644 --- a/jellyfinstats/audio.py +++ b/jellyfinstats/audio.py @@ -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.*) - (?P.*) \((?P.*)\)') # 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) diff --git a/jellyfinstats/cache.py b/jellyfinstats/cache.py new file mode 100644 index 0000000..6cb318d --- /dev/null +++ b/jellyfinstats/cache.py @@ -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 diff --git a/jellyfinstats/config.py b/jellyfinstats/config.py index 2adecf8..05ce2c7 100644 --- a/jellyfinstats/config.py +++ b/jellyfinstats/config.py @@ -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' diff --git a/jellyfinstats/db.py b/jellyfinstats/db.py index 1174aca..646524a 100644 --- a/jellyfinstats/db.py +++ b/jellyfinstats/db.py @@ -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()] diff --git a/jellyfinstats/language/jellyfinStats.po b/jellyfinstats/language/jellyfinStats.po new file mode 100644 index 0000000..8285e03 --- /dev/null +++ b/jellyfinstats/language/jellyfinStats.po @@ -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 , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: jellyfinStats 1.0\n" +"Report-Msgid-Bugs-To: root@lifegpc.com\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 \n" +"Language-Team: LANGUAGE \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 "" diff --git a/jellyfinstats/language/zh_CN/LC_MESSAGES/jellyfinStats.po b/jellyfinstats/language/zh_CN/LC_MESSAGES/jellyfinStats.po new file mode 100644 index 0000000..b192f08 --- /dev/null +++ b/jellyfinstats/language/zh_CN/LC_MESSAGES/jellyfinStats.po @@ -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 , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: jellyfinStats 1.0\n" +"Report-Msgid-Bugs-To: root@lifegpc.com\n" +"POT-Creation-Date: 2024-05-15 13:56+0800\n" +"PO-Revision-Date: 2024-05-15 10:20+0800\n" +"Last-Translator: mhy \n" +"Language-Team: Chinese (simplified) \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 "最后一页" diff --git a/jellyfinstats/utils.py b/jellyfinstats/utils.py new file mode 100644 index 0000000..6450dc0 --- /dev/null +++ b/jellyfinstats/utils.py @@ -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] diff --git a/update_i18n.sh b/update_i18n.sh new file mode 100755 index 0000000..354826d --- /dev/null +++ b/update_i18n.sh @@ -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 'root@lifegpc.com' +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