-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcache.py
More file actions
172 lines (136 loc) · 5.66 KB
/
Copy pathcache.py
File metadata and controls
172 lines (136 loc) · 5.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
"""MusicDeLoc キャッシュ管理モジュール"""
from __future__ import annotations
from dataclasses import dataclass, asdict
from datetime import datetime
from pathlib import Path
from typing import Optional, Literal
import json
from exceptions import CacheError
ActionType = Literal["convert", "skip", "not_found", "manual"]
@dataclass
class CachedEntry:
"""キャッシュされたアーティスト情報"""
action: ActionType
musicbrainz_name: Optional[str]
mbid: Optional[str]
checked_at: str
def to_dict(self) -> dict:
return asdict(self)
@classmethod
def from_dict(cls, data: dict) -> "CachedEntry":
return cls(
action=data["action"],
musicbrainz_name=data.get("musicbrainz_name"),
mbid=data.get("mbid"),
checked_at=data["checked_at"],
)
class CacheManager:
"""アーティスト名マッピングのキャッシュ管理"""
VERSION = "1.0"
DEFAULT_DIR = Path.home() / ".musicdeloc"
def __init__(self, cache_path: Optional[Path] = None):
self.cache_path = cache_path or (self.DEFAULT_DIR / "cache.json")
self._entries: dict[str, CachedEntry] = {}
self._load()
def _ensure_dir(self) -> None:
"""キャッシュディレクトリを作成"""
self.cache_path.parent.mkdir(parents=True, exist_ok=True)
def _load(self) -> None:
"""キャッシュファイルを読み込み"""
if not self.cache_path.exists():
return
try:
with open(self.cache_path, "r", encoding="utf-8") as f:
data = json.load(f)
if data.get("version") != self.VERSION:
# バージョン不一致の場合は空で開始
return
entries = data.get("entries", {})
for artist_name, entry_data in entries.items():
self._entries[artist_name] = CachedEntry.from_dict(entry_data)
except (json.JSONDecodeError, KeyError) as e:
raise CacheError(f"キャッシュファイルの読み込みに失敗: {e}")
def _save(self) -> None:
"""キャッシュファイルを保存"""
self._ensure_dir()
data = {
"version": self.VERSION,
"entries": {name: entry.to_dict() for name, entry in self._entries.items()},
}
try:
with open(self.cache_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
except IOError as e:
raise CacheError(f"キャッシュファイルの保存に失敗: {e}")
def get(self, artist_name: str) -> Optional[CachedEntry]:
"""キャッシュからエントリを取得"""
return self._entries.get(artist_name)
def set(
self,
artist_name: str,
action: ActionType,
musicbrainz_name: Optional[str] = None,
mbid: Optional[str] = None,
) -> None:
"""エントリをキャッシュに保存"""
self._entries[artist_name] = CachedEntry(
action=action,
musicbrainz_name=musicbrainz_name,
mbid=mbid,
checked_at=datetime.now().isoformat(),
)
self._save()
def set_convert(
self, artist_name: str, musicbrainz_name: str, mbid: Optional[str] = None
) -> None:
"""変換エントリを保存"""
self.set(artist_name, "convert", musicbrainz_name, mbid)
def set_skip(
self, artist_name: str, musicbrainz_name: str, mbid: Optional[str] = None
) -> None:
"""スキップエントリを保存(正式名と一致)"""
self.set(artist_name, "skip", musicbrainz_name, mbid)
def set_not_found(self, artist_name: str) -> None:
"""見つからなかったエントリを保存"""
self.set(artist_name, "not_found", None, None)
def set_manual(
self, artist_name: str, musicbrainz_name: str, mbid: Optional[str] = None
) -> None:
"""手動入力エントリを保存"""
self.set(artist_name, "manual", musicbrainz_name, mbid)
def remove(self, artist_name: str) -> bool:
"""キャッシュからエントリを削除"""
if artist_name in self._entries:
del self._entries[artist_name]
self._save()
return True
return False
def clear(self) -> None:
"""キャッシュをクリア"""
self._entries.clear()
self._save()
def get_all(self) -> dict[str, CachedEntry]:
"""全エントリを取得"""
return self._entries.copy()
def get_pending(self, all_artists: set[str]) -> set[str]:
"""キャッシュにない(未処理の)アーティスト名を取得"""
return all_artists - set(self._entries.keys())
def get_conversions(self) -> dict[str, str]:
"""変換対象のマッピングを取得(artist_name -> musicbrainz_name)"""
return {
name: entry.musicbrainz_name
for name, entry in self._entries.items()
if entry.action in ("convert", "manual") and entry.musicbrainz_name
}
def get_skipped(self) -> list[str]:
"""スキップされたアーティスト名を取得"""
return [name for name, entry in self._entries.items() if entry.action == "skip"]
def get_not_found(self) -> list[str]:
"""見つからなかったアーティスト名を取得"""
return [
name for name, entry in self._entries.items() if entry.action == "not_found"
]
def __len__(self) -> int:
return len(self._entries)
def __contains__(self, artist_name: str) -> bool:
return artist_name in self._entries