Add code
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -158,3 +158,5 @@ cython_debug/
|
|||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
config.yaml
|
||||||
|
|||||||
0
jellyfinstats/__init__.py
Normal file
0
jellyfinstats/__init__.py
Normal file
16
jellyfinstats/__main__.py
Normal file
16
jellyfinstats/__main__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from argparse import ArgumentParser
|
||||||
|
from .audio import generate_audio_report
|
||||||
|
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.")
|
||||||
|
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)
|
||||||
39
jellyfinstats/audio.py
Normal file
39
jellyfinstats/audio.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
from .db import PlaybackReportingDb, LibraryDb
|
||||||
|
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):
|
||||||
|
offset = 0
|
||||||
|
data = pdb.get_activitys(offset, itemType='Audio')
|
||||||
|
count = 0
|
||||||
|
while len(data) > 0:
|
||||||
|
for d in data:
|
||||||
|
itemId = d['ItemId']
|
||||||
|
item = ldb.get_item(itemId)
|
||||||
|
if item:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
itemName = d['ItemName']
|
||||||
|
re = ITEMNAME_PATTERN.match(itemName)
|
||||||
|
if re is None:
|
||||||
|
raise ValueError(f"Failed to parse ItemName: {itemName}")
|
||||||
|
re = re.groupdict()
|
||||||
|
if re['album_artist'] == 'Not Known':
|
||||||
|
re['album_artist'] = None
|
||||||
|
if re['album'] == 'Not Known':
|
||||||
|
re['album'] = None
|
||||||
|
items = ldb.get_audios(re['track'], re['album'])
|
||||||
|
if len(items) == 1:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if len(items):
|
||||||
|
print(items)
|
||||||
|
else:
|
||||||
|
print(re)
|
||||||
|
count += 1
|
||||||
|
offset += len(data)
|
||||||
|
data = pdb.get_activitys(offset, itemType='Audio')
|
||||||
|
print('Count', count)
|
||||||
47
jellyfinstats/config.py
Normal file
47
jellyfinstats/config.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from argparse import Namespace
|
||||||
|
try:
|
||||||
|
from functools import cached_property
|
||||||
|
except ImportError:
|
||||||
|
cached_property = property
|
||||||
|
from os.path import join
|
||||||
|
from yaml import load as loadyaml
|
||||||
|
try:
|
||||||
|
from yaml import CSafeLoader as SafeLoader
|
||||||
|
except ImportError:
|
||||||
|
from yaml import SafeLoader
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
def __init__(self, path: str, args: Namespace = None):
|
||||||
|
with open(path, encoding="UTF-8") as f:
|
||||||
|
self._data = loadyaml(f, Loader=SafeLoader)
|
||||||
|
self._args = args
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def playback_reporting_db(self) -> str:
|
||||||
|
if self._args and self._args.playback_reporting_db:
|
||||||
|
return self._args.playback_reporting_db
|
||||||
|
if 'playback_reporting_db' in self._data and self._data['playback_reporting_db']: # noqa: E501
|
||||||
|
return self._data['playback_reporting_db']
|
||||||
|
d = self.jellyfin_data_dir
|
||||||
|
if d:
|
||||||
|
return join(d, "playback_reporting.db")
|
||||||
|
raise ValueError('playback_reporting.db not set.')
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def library_db(self) -> str:
|
||||||
|
if self._args and self._args.library_db:
|
||||||
|
return self._args.library_db
|
||||||
|
if 'library_db' in self._data and self._data['library_db']:
|
||||||
|
return self._data['library_db']
|
||||||
|
d = self.jellyfin_data_dir
|
||||||
|
if d:
|
||||||
|
return join(d, "library.db")
|
||||||
|
raise ValueError('library.db not set.')
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def jellyfin_data_dir(self) -> str | None:
|
||||||
|
if self._args and self._args.jellyfin_data_dir:
|
||||||
|
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']
|
||||||
71
jellyfinstats/db.py
Normal file
71
jellyfinstats/db.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
class PlaybackReportingDb:
|
||||||
|
def __init__(self, fn: str):
|
||||||
|
self._db = sqlite3.connect(fn)
|
||||||
|
self._closed = False
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, tp, val, trace):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self._closed:
|
||||||
|
return
|
||||||
|
self._db.close()
|
||||||
|
self._closed = True
|
||||||
|
|
||||||
|
def get_activitys(self, offset: int = 0, limit: int = 100,
|
||||||
|
itemType: str = None):
|
||||||
|
where_sql = ''
|
||||||
|
args = []
|
||||||
|
if itemType is not None:
|
||||||
|
where_sql = ' WHERE ItemType = ?'
|
||||||
|
args.append(itemType)
|
||||||
|
args.append(limit)
|
||||||
|
args.append(offset)
|
||||||
|
cur = self._db.execute(f"SELECT * FROM PlaybackActivity{where_sql} LIMIT ? OFFSET ?;", args) # noqa: E501
|
||||||
|
cur.row_factory = sqlite3.Row
|
||||||
|
return [dict(i) for i in cur.fetchall()]
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryDb:
|
||||||
|
def __init__(self, fn: str):
|
||||||
|
self._db = sqlite3.connect(fn)
|
||||||
|
self._closed = False
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, tp, val, trace):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self._closed:
|
||||||
|
return
|
||||||
|
self._db.close()
|
||||||
|
self._closed = True
|
||||||
|
|
||||||
|
def get_items(self, offset: int = 0, limit: int = 100):
|
||||||
|
cur = self._db.execute("SELECT * FROM TypedBaseItems LIMIT ? OFFSET ?;", [limit, offset]) # noqa: E501
|
||||||
|
cur.row_factory = sqlite3.Row
|
||||||
|
return [dict(i) for i in cur.fetchall()]
|
||||||
|
|
||||||
|
def get_item(self, itemId: str):
|
||||||
|
cur = self._db.execute("SELECT * FROM TypedBaseItems WHERE PresentationUniqueKey = ?;", [itemId]) # noqa: E501
|
||||||
|
cur.row_factory = sqlite3.Row
|
||||||
|
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]
|
||||||
|
where_sql = ''
|
||||||
|
if album is not None:
|
||||||
|
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.row_factory = sqlite3.Row
|
||||||
|
return [dict(i) for i in cur.fetchall()]
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pyyaml
|
||||||
Reference in New Issue
Block a user