netease-music-cli/scripts/lib/snapshot.py
2026-10-02 07:26:46 +08:00

130 lines
4.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""队列快照:把「当前队列」存成可回放的文件。
只存歌曲列表(id/歌名/歌手/时长),不存播放地址 —— 那种 URL 二十分钟就过期。
回放时按列表重新取地址,所以快照可以长期留着。
用法:
snapshot.py save <queue.json> <快照文件> <位置> <shuffle:true|false> [名字]
snapshot.py list <快照目录> 列出全部快照(JSON 数组)
snapshot.py songs <快照文件> 只打印歌曲列表(喂给 build_queue)
snapshot.py meta <快照文件> 打印元信息(名字/首数/位置/shuffle/来源/时间)
"""
import datetime
import json
import os
import sys
KEEP_NAMED = 20 # 命名快照最多留 20 条,多的按时间删最旧(prev.json 不受影响)
def _now():
return datetime.datetime.now().astimezone().isoformat(timespec='seconds')
def cmd_save(queue_path, out_path, pos, shuffle, name='', song_id=''):
try:
q = json.load(open(queue_path, encoding='utf-8'))
except Exception:
return 2
songs = q.get('songs') or []
if not songs:
return 2
out = {
'name': name or q.get('source') or 'unnamed',
'saved_at': _now(),
'source': q.get('source') or '',
'pos': int(pos or -1),
'shuffle': str(shuffle).lower() == 'true',
# 开随机后 pos 是打乱顺序里的位置,回放靠 song_id 认「当时在放的那首」
'song_id': int(song_id) if str(song_id).strip().isdigit() else None,
'count': len(songs),
'songs': songs,
}
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, 'w', encoding='utf-8') as f:
json.dump(out, f, ensure_ascii=False)
_prune(os.path.dirname(out_path))
print('%s\t%d\t%s' % (out['name'], out['count'], out['saved_at']))
return 0
def _prune(d):
named = []
for fn in os.listdir(d):
if not fn.endswith('.json') or fn.startswith('.') or fn == 'prev.json':
continue
try:
meta = json.load(open(os.path.join(d, fn), encoding='utf-8'))
except Exception:
continue
named.append((meta.get('saved_at') or '', fn))
named.sort()
for _, fn in named[:-KEEP_NAMED] if len(named) > KEEP_NAMED else []:
try:
os.remove(os.path.join(d, fn))
except OSError:
pass
def cmd_list(d):
rows = []
for fn in sorted(os.listdir(d)) if os.path.isdir(d) else []:
if not fn.endswith('.json') or fn.startswith('.'):
continue
try:
meta = json.load(open(os.path.join(d, fn), encoding='utf-8'))
except Exception:
continue
rows.append({
'file': fn,
'name': meta.get('name') or fn[:-5],
'count': meta.get('count') or len(meta.get('songs') or []),
'saved_at': meta.get('saved_at') or '',
'source': meta.get('source') or '',
'pos': meta.get('pos', -1),
})
rows.sort(key=lambda r: (r['file'] != 'prev.json', r['saved_at']), reverse=False)
json.dump(rows, sys.stdout, ensure_ascii=False)
return 0
def cmd_songs(p):
try:
d = json.load(open(p, encoding='utf-8'))
except Exception:
return 4
json.dump(d.get('songs') or [], sys.stdout, ensure_ascii=False)
return 0
def cmd_meta(p):
try:
d = json.load(open(p, encoding='utf-8'))
except Exception:
return 4
json.dump({k: d.get(k) for k in ('name', 'count', 'pos', 'shuffle', 'song_id', 'source', 'saved_at')},
sys.stdout, ensure_ascii=False)
return 0
def main():
if len(sys.argv) < 3:
print(__doc__.strip().splitlines()[0], file=sys.stderr)
return 2
cmd, args = sys.argv[1], sys.argv[2:]
if cmd == 'save':
return cmd_save(*(args + ['', '', '', '', ''])[:6])
if cmd == 'list':
return cmd_list(args[0])
if cmd == 'songs':
return cmd_songs(args[0])
if cmd == 'meta':
return cmd_meta(args[0])
print('未知子命令:%s' % cmd, file=sys.stderr)
return 2
if __name__ == '__main__':
sys.exit(main())