Update code

This commit is contained in:
2024-05-16 10:57:24 +08:00
parent 4f133e976d
commit f6582be3f0
7 changed files with 382 additions and 76 deletions

View File

@@ -30,4 +30,4 @@ with PlaybackReportingDb(cfg.playback_reporting_db) as pdb:
output = join(cfg.output_dir, 'audio', username)
maxDate = u['MaxDate']
minDate = u['MinDate']
generate_audio_report(pdb, re[0], re[1], output, userid)
generate_audio_report(pdb, re[0], re[1], re[2], output, userid)

View File

@@ -3,14 +3,17 @@ from .cache import IdRelativeCache
from .config import Config
from .csv import CSVFile
from .db import PlaybackReportingDb, LibraryDb
from .utils import ask_choice, parse_time
from .utils import ask_choice, format_duration, parse_time
from datetime import datetime
from re import compile
from os import makedirs
from os.path import join
from math import floor
ITEMNAME_PATTERN = compile(r'(?P<album_artist>.*) - (?P<track>.*) \((?P<album>.*)\)') # noqa: E501
NOT_KNOWN = "Not Known"
TIME_BASE = 10_000_000
def print_item(item):
@@ -155,9 +158,9 @@ def prepare_audio_map(pdb: PlaybackReportingDb, ldb: LibraryDb,
if re is None:
raise ValueError(f"Failed to parse ItemName: {itemName}")
re = re.groupdict()
if re['album_artist'] == 'Not Known':
if re['album_artist'] == NOT_KNOWN:
re['album_artist'] = None
if re['album'] == 'Not Known':
if re['album'] == NOT_KNOWN:
re['album'] = None
items = ldb.get_audios(re['track'], re['album'])
if len(items) == 1:
@@ -184,18 +187,57 @@ def prepare_audio_map(pdb: PlaybackReportingDb, ldb: LibraryDb,
rowMap[rowid] = itemId
offset += len(data)
data = pdb.get_activitys(offset, itemType='Audio')
return itemMap, rowMap
albumMap = {}
for itemId in itemMap:
item = itemMap[itemId]
album = ''
album_artists = ''
if 'type' in item:
album = item['Album']
album_artists = item['AlbumArtists']
else:
it = ITEMNAME_PATTERN.match(item['ItemName']).groupdict()
if it['album'] != NOT_KNOWN:
album = it['album']
if it['album_artist'] != NOT_KNOWN:
album_artists = it['album_artist']
if album:
if album not in albumMap:
album_artists = album_artists if album_artists else None
items = ldb.get_albums(album, album_artists)
if len(items) == 0:
items = ldb.get_albums(album)
if len(items) == 1:
albumMap[album] = items[0]
elif len(items) > 1:
print(len(items))
raise NotImplementedError('FIX ME')
else:
data = {'name': album}
if 'type' in item:
data['album_artists'] = item['AlbumArtists']
data['date'] = item['PremiereDate']
data['year'] = item['ProductionYear']
data['publisher'] = item['Studios']
else:
data['album_artists'] = album_artists
data['date'] = None
data['year'] = None
data['publisher'] = None
albumMap[album] = data
return itemMap, rowMap, albumMap
def generate_audio_report(pdb: PlaybackReportingDb, itemMap, rowMap,
def generate_audio_report(pdb: PlaybackReportingDb, itemMap, rowMap, albumMap,
output: str, userId: str = None,
startTime: float = None, endTime: float = None):
makedirs(output, exist_ok=True)
offset = 0
data = pdb.get_activitys(offset, itemType='Audio', userId=userId,
startTime=startTime, endTime=endTime)
albumCountMap = {}
with CSVFile(join(output, "history.csv")) as his:
his.write(_("Id"), _("Date"), _("Time"), _("Name"), _("Artists"), _("Album"), _("Album artists"), _("Original item id"), _("Item id"), _("Play Duration"), _("Record content"), _("Client name"), _("Device name"), _("Playback method")) # noqa: E501
his.write(_("Id"), _("Date"), _("Time"), _("Name"), _("Artists"), _("Album"), _("Album artists"), _("Duration"), _("Duration") + _("(seconds)"), _("Original item id"), _("Item id"), _("Play duration"), _("Play duration") + _("(seconds)"), _("Record content"), _("Client name"), _("Device name"), _("Playback method"), _("Play count")) # noqa: E501
while len(data) > 0:
for i in data:
rowid = i['rowid']
@@ -211,19 +253,66 @@ def generate_audio_report(pdb: PlaybackReportingDb, itemMap, rowMap,
album_artists = ''
original_item_id = i['ItemId']
play_duration = i['PlayDuration']
duration = None
play_count = 1
if 'type' in item:
name = item['Name']
artists = item['Artists']
album = item['Album']
album_artists = item['AlbumArtists']
duration = item['RunTimeTicks'] / 10_000_000
play_count = floor(play_duration / duration)
extrad = play_duration % duration
if extrad > 60 or extrad > duration * 0.95:
play_count += 1
else:
it = ITEMNAME_PATTERN.match(item['ItemName']).groupdict()
name = it['track']
if it['album'] != 'Not Known':
if it['album'] != NOT_KNOWN:
album = it['album']
if it['album_artist'] != "Not Known":
if it['album_artist'] != NOT_KNOWN:
album_artists = it['album_artist']
his.write(rowid, date, time, name, artists, album, album_artists, original_item_id, itemId, play_duration, i['ItemName'], i['ClientName'], i['DeviceName'], i['PlaybackMethod']) # noqa: E501
his.write(rowid, date, time, name, artists, album, album_artists, format_duration(duration), duration, original_item_id, itemId, format_duration(play_duration), play_duration, i['ItemName'], i['ClientName'], i['DeviceName'], i['PlaybackMethod'], play_count) # noqa: E501
if album:
if album in albumCountMap:
tmp = albumCountMap[album]
tmp['count'] += 1
tmp['play_count'] += play_count
tmp['duration'] += play_duration
else:
albumCountMap[album] = {'count': 1,
'play_count': play_count,
'duration': play_duration}
offset += len(data)
data = pdb.get_activitys(offset, itemType='Audio', userId=userId,
startTime=startTime, endTime=endTime)
with CSVFile(join(output, 'album.csv')) as al:
al.write(_("Name"), _("Album artists"), _("Artists"), _("Record count"), _("Play count"), _("Play duration"), _("Play duration") + _("(seconds)"), _("Duration"), _("Duration") + _("(seconds)"), _("Year"), _("Publish date"), _("Publisher"), _("Item id")) # noqa: E501
for album in albumCountMap:
if album not in albumMap:
continue
item = albumMap[album]
album_artists = ''
artists = ''
count = albumCountMap[album]
duration = None
year = None
publisher = None
itemId = None
if 'type' in item:
album_artists = item['AlbumArtists']
artists = item['Artists']
duration = item['RunTimeTicks'] / TIME_BASE
year = item['ProductionYear']
date = item['PremiereDate']
publisher = item['Studios']
itemId = item['PresentationUniqueKey']
else:
album_artists = item['album_artists']
year = item['year']
date = item['date']
publisher = item['publisher']
if year and date:
if date.endswith("-01-01 00:00:00"):
date = None
al.write(album, album_artists, artists, count['count'], count['play_count'], format_duration(count['duration']), count['duration'], format_duration(duration), duration, year, date, publisher, itemId) # noqa: E501

View File

@@ -58,7 +58,9 @@ def writeField(f: IO[bytes], *k):
f.write(b',')
else:
a = True
if isinstance(i, bool):
if i is None:
i = ''
elif isinstance(i, bool):
i = str(int(i))
elif isinstance(i, Enum):
i = str(i.value)

View File

@@ -97,6 +97,19 @@ class LibraryDb:
cur.row_factory = sqlite3.Row
return [dict(i) for i in cur.fetchall()]
def get_albums(self, album: str = None, albumArtists: str = None):
args = ['MediaBrowser.Controller.Entities.Audio.MusicAlbum']
where_sql = ''
if album is not None:
where_sql += ' AND Name = ?'
args.append(album)
if albumArtists is not None:
where_sql += ' AND AlbumArtists = ?'
args.append(albumArtists)
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()]
class JellyfinDb:
def __init__(self, fn: str):

View File

@@ -8,7 +8,7 @@ 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"
"POT-Creation-Date: 2024-05-16 10:47+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"
@@ -17,102 +17,190 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: __main__.py:10
#: __main__.py:20
msgid "The path to config file."
msgstr ""
#: __main__.py:11
#: __main__.py:21
msgid "The path to playback_reporting.db"
msgstr ""
#: __main__.py:12
#: __main__.py:22
msgid "The path to library.db"
msgstr ""
#: __main__.py:13
#: __main__.py:23
msgid "The path to jellyfin data directory."
msgstr ""
#: __main__.py:14
#: __main__.py:24
msgid "The directory for output files."
msgstr ""
#: __main__.py:15
#: __main__.py:25
msgid "Specify maximum items to display in one page."
msgstr ""
#: audio.py:15 audio.py:35
#: __main__.py:26
msgid "The path to jellyfin.db"
msgstr ""
#: audio.py:22 audio.py:42
msgid "Album: "
msgstr ""
#: audio.py:17
#: audio.py:24
msgid "Artists: "
msgstr ""
#: audio.py:19 audio.py:37
#: audio.py:26 audio.py:44
msgid "Album artists: "
msgstr ""
#: audio.py:33
#: audio.py:40
msgid "Original item: "
msgstr ""
#: audio.py:40 audio.py:77
#: audio.py:47 audio.py:84
msgid "Please choose audio item:"
msgstr ""
#: audio.py:40 audio.py:48 audio.py:74
#: audio.py:47 audio.py:55 audio.py:81
msgid "No item"
msgstr ""
#: audio.py:40
#: audio.py:47
msgid "Choose other items"
msgstr ""
#: audio.py:48
#: audio.py:55
msgid "Input item id"
msgstr ""
#: audio.py:48
#: audio.py:55
msgid "Input track name"
msgstr ""
#: audio.py:48
#: audio.py:55
msgid "Input album name"
msgstr ""
#: audio.py:50 audio.py:76
#: audio.py:57 audio.py:83
msgid "Choose in given choices"
msgstr ""
#: audio.py:51
#: audio.py:58
msgid "Please choose action: "
msgstr ""
#: audio.py:64
#: audio.py:71
msgid "Please input item id:"
msgstr ""
#: audio.py:69
#: audio.py:76
msgid "Item not found."
msgstr ""
#: audio.py:74
#: audio.py:81
msgid "Back"
msgstr ""
#: audio.py:85
#: audio.py:92
msgid "Items not found."
msgstr ""
#: audio.py:89
#: audio.py:96
msgid "Please input track name:"
msgstr ""
#: audio.py:94
#: audio.py:101
msgid "Please input album name:"
msgstr ""
#: audio.py:240
msgid "Id"
msgstr ""
#: audio.py:240
msgid "Date"
msgstr ""
#: audio.py:240
msgid "Time"
msgstr ""
#: audio.py:240 audio.py:290
msgid "Name"
msgstr ""
#: audio.py:240 audio.py:290
msgid "Artists"
msgstr ""
#: audio.py:240
msgid "Album"
msgstr ""
#: audio.py:240 audio.py:290
msgid "Album artists"
msgstr ""
#: audio.py:240 audio.py:290
msgid "Duration"
msgstr ""
#: audio.py:240 audio.py:290
msgid "(seconds)"
msgstr ""
#: audio.py:240
msgid "Original item id"
msgstr ""
#: audio.py:240 audio.py:290
msgid "Item id"
msgstr ""
#: audio.py:240 audio.py:290
msgid "Play duration"
msgstr ""
#: audio.py:240
msgid "Record content"
msgstr ""
#: audio.py:240
msgid "Client name"
msgstr ""
#: audio.py:240
msgid "Device name"
msgstr ""
#: audio.py:240
msgid "Playback method"
msgstr ""
#: audio.py:240 audio.py:290
msgid "Play count"
msgstr ""
#: audio.py:290
msgid "Record count"
msgstr ""
#: audio.py:290
msgid "Year"
msgstr ""
#: audio.py:290
msgid "Publish date"
msgstr ""
#: audio.py:290
msgid "Publisher"
msgstr ""
#: cache.py:23
msgid "Unsupported version: "
msgstr ""
@@ -121,32 +209,37 @@ msgstr ""
msgid "Failed to load cache."
msgstr ""
#: config.py:40 config.py:51
#: config.py:40 config.py:51 config.py:69
#, python-format
msgid "%s not set."
msgstr ""
#: utils.py:6
#: utils.py:11
msgid "Please choose: "
msgstr ""
#: utils.py:23
#: utils.py:28
#, python-format
msgid "Page %i/%i"
msgstr ""
#: utils.py:31
#: utils.py:36
msgid "First page"
msgstr ""
#: utils.py:33
#: utils.py:38
msgid "Previous page"
msgstr ""
#: utils.py:36
#: utils.py:41
msgid "Next page"
msgstr ""
#: utils.py:38
#: utils.py:43
msgid "Last page"
msgstr ""
#: utils.py:97
#, python-format
msgid "%i day"
msgstr ""

View File

@@ -7,7 +7,7 @@ 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"
"POT-Creation-Date: 2024-05-16 10:47+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"
@@ -16,102 +16,190 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: __main__.py:10
#: __main__.py:20
msgid "The path to config file."
msgstr "配置文件位置。"
#: __main__.py:11
#: __main__.py:21
msgid "The path to playback_reporting.db"
msgstr "playback_reporting.db 文件位置。"
#: __main__.py:12
#: __main__.py:22
msgid "The path to library.db"
msgstr "library.db 文件位置。"
#: __main__.py:13
#: __main__.py:23
msgid "The path to jellyfin data directory."
msgstr "Jellyfin 数据目录位置。"
#: __main__.py:14
#: __main__.py:24
msgid "The directory for output files."
msgstr "输出目录位置。"
#: __main__.py:15
#: __main__.py:25
msgid "Specify maximum items to display in one page."
msgstr "指定一页可以显示的最大条目数。"
#: audio.py:15 audio.py:35
#: __main__.py:26
msgid "The path to jellyfin.db"
msgstr "jellyfin.db 文件位置。"
#: audio.py:22 audio.py:42
msgid "Album: "
msgstr "专辑:"
#: audio.py:17
#: audio.py:24
msgid "Artists: "
msgstr "艺术家:"
#: audio.py:19 audio.py:37
#: audio.py:26 audio.py:44
msgid "Album artists: "
msgstr "专辑艺术家:"
#: audio.py:33
#: audio.py:40
msgid "Original item: "
msgstr "原项目:"
#: audio.py:40 audio.py:77
#: audio.py:47 audio.py:84
msgid "Please choose audio item:"
msgstr "请选择音乐项目:"
#: audio.py:40 audio.py:48 audio.py:74
#: audio.py:47 audio.py:55 audio.py:81
msgid "No item"
msgstr "无对应项目"
#: audio.py:40
#: audio.py:47
msgid "Choose other items"
msgstr "选择其他项目"
#: audio.py:48
#: audio.py:55
msgid "Input item id"
msgstr "输入项目ID"
#: audio.py:48
#: audio.py:55
msgid "Input track name"
msgstr "输入标题"
#: audio.py:48
#: audio.py:55
msgid "Input album name"
msgstr "输入专辑名称"
#: audio.py:50 audio.py:76
#: audio.py:57 audio.py:83
msgid "Choose in given choices"
msgstr "从给出的选项中选择"
#: audio.py:51
#: audio.py:58
msgid "Please choose action: "
msgstr "请选择方式:"
#: audio.py:64
#: audio.py:71
msgid "Please input item id:"
msgstr "请输入项目ID:"
#: audio.py:69
#: audio.py:76
msgid "Item not found."
msgstr "没有找到项目。"
#: audio.py:74
#: audio.py:81
msgid "Back"
msgstr "返回"
#: audio.py:85
#: audio.py:92
msgid "Items not found."
msgstr "没有找到项目。"
#: audio.py:89
#: audio.py:96
msgid "Please input track name:"
msgstr "请输入标题:"
#: audio.py:94
#: audio.py:101
msgid "Please input album name:"
msgstr "请输入专辑名称:"
#: audio.py:240
msgid "Id"
msgstr "ID"
#: audio.py:240
msgid "Date"
msgstr "日期"
#: audio.py:240
msgid "Time"
msgstr "时间"
#: audio.py:240 audio.py:290
msgid "Name"
msgstr "名称"
#: audio.py:240 audio.py:290
msgid "Artists"
msgstr "艺术家"
#: audio.py:240
msgid "Album"
msgstr "专辑"
#: audio.py:240 audio.py:290
msgid "Album artists"
msgstr "专辑艺术家"
#: audio.py:240 audio.py:290
msgid "Duration"
msgstr "时长"
#: audio.py:240 audio.py:290
msgid "(seconds)"
msgstr "(秒)"
#: audio.py:240
msgid "Original item id"
msgstr "原项目ID"
#: audio.py:240 audio.py:290
msgid "Item id"
msgstr "项目ID"
#: audio.py:240 audio.py:290
msgid "Play duration"
msgstr "播放时长"
#: audio.py:240
msgid "Record content"
msgstr "记录内容"
#: audio.py:240
msgid "Client name"
msgstr "客户端名称"
#: audio.py:240
msgid "Device name"
msgstr "设备名称"
#: audio.py:240
msgid "Playback method"
msgstr "播放方式"
#: audio.py:240 audio.py:290
msgid "Play count"
msgstr "播放次数"
#: audio.py:290
msgid "Record count"
msgstr "记录次数"
#: audio.py:290
msgid "Year"
msgstr "年份"
#: audio.py:290
msgid "Publish date"
msgstr "发布日期"
#: audio.py:290
msgid "Publisher"
msgstr "发布者"
#: cache.py:23
msgid "Unsupported version: "
msgstr "不支持的版本:"
@@ -120,32 +208,37 @@ msgstr "不支持的版本:"
msgid "Failed to load cache."
msgstr "加载缓存失败。"
#: config.py:40 config.py:51
#: config.py:40 config.py:51 config.py:69
#, python-format
msgid "%s not set."
msgstr "%s 未设置。"
#: utils.py:6
#: utils.py:11
msgid "Please choose: "
msgstr "请选择:"
#: utils.py:23
#: utils.py:28
#, python-format
msgid "Page %i/%i"
msgstr "第%i/%i页"
#: utils.py:31
#: utils.py:36
msgid "First page"
msgstr "第一页"
#: utils.py:33
#: utils.py:38
msgid "Previous page"
msgstr "上一页"
#: utils.py:36
#: utils.py:41
msgid "Next page"
msgstr "下一页"
#: utils.py:38
#: utils.py:43
msgid "Last page"
msgstr "最后一页"
#: utils.py:97
#, python-format
msgid "%i day"
msgstr "%i 天"

View File

@@ -1,4 +1,4 @@
from math import ceil
from math import ceil, floor
from datetime import datetime, timezone
from re import compile
from . import _
@@ -87,3 +87,19 @@ def parse_time(time: str) -> float:
def convert_uid(uid: str) -> str:
t = uid.upper()
return f"{t[:8]}-{t[8:12]}-{t[12:16]}-{t[16:20]}-{t[20:]}"
def format_duration(duration: float | None) -> str:
if duration is None:
return ''
duration = round(duration)
re = ''
if duration >= 86400:
re += _("%i day") % (floor(duration / 86400)) + " "
duration %= 86400
if duration >= 3600:
re += str(floor(duration / 3600)).rjust(2, "0") + ":"
duration %= 3600
min = str(floor(duration / 60)).rjust(2, "0")
sec = str(duration % 60).rjust(2, "0")
return f"{re}{min}:{sec}"