music-mdata 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mdata.py ADDED
@@ -0,0 +1,285 @@
1
+ # Music Metadata • copyright blasieuwu, 2026
2
+ # a better way of getting track metadata.
3
+ from pathlib import Path
4
+ from typing import Any
5
+ import sys, re
6
+
7
+ REQUIRED = {
8
+ "title",
9
+ "artist",
10
+ "album",
11
+ "explicit"
12
+ }
13
+
14
+ def validate_mdata(file_name: str, silent: bool = False, yields: bool = False, track_identity=None) -> dict | None:
15
+ directory = Path(__file__).parent.resolve()
16
+ file_path = Path(directory / file_name).resolve()
17
+ file_ext = Path(file_name).suffix
18
+
19
+ if not file_path.exists():
20
+ if not silent:
21
+ print(f"\x1b[1;31m[1/2] [-] file wasn't located.\x1b[0m")
22
+ return
23
+
24
+ if file_ext != ".mdata":
25
+ if not silent:
26
+ print(f"\x1b[1;33m[2/2] [!] '{file_name}' is not a .mdata file...\x1b[0m")
27
+ return
28
+
29
+ if not silent:
30
+ print(f"\n\x1b[1;32m--- processing {file_name} ---\x1b[0m\n")
31
+ data = {}
32
+ errors = []
33
+ warnings = []
34
+ identified = False
35
+
36
+ lines = file_path.read_text(encoding="utf-8").splitlines()
37
+
38
+ for line_num, line in enumerate(lines, 1):
39
+ line = line.strip()
40
+
41
+ # discard empty lines
42
+ if not line:
43
+ continue
44
+
45
+ # syntax check // square brackets
46
+ if not (line.startswith("[") and line.endswith("]")) or ":" not in line:
47
+ errors.append(f"[{file_name}:{line_num}] invalid syntax -> '{line}'")
48
+ continue
49
+
50
+ content = line[1:-1].strip()
51
+ key, value = content.split(":", 1)
52
+ key, value = key.strip().lower(), value.strip()
53
+
54
+ # syntax check // key/value args
55
+ if not key or not value:
56
+ errors.append(f"[{file_name}:{line_num}] empty key/value in '{line}'")
57
+ continue
58
+
59
+ # metadata checks
60
+ if key == "explicit":
61
+ if value.lower() not in ("true", "false"):
62
+ errors.append(f"[{file_name}:{line_num}] 'explicit' must be 'true' or 'false'.")
63
+ else:
64
+ data[key] = value.lower() == "true"
65
+
66
+ elif key in ("search-links", "playback-links"):
67
+ links = [url.strip() for url in value.split("|")]
68
+ data[key] = links
69
+
70
+ elif key in ("artist", "romanized-artist"):
71
+ data[key] = [a.strip() for a in value.split(",")]
72
+
73
+ elif key in ("title", "romanized-title", "album"):
74
+ data[key] = value
75
+
76
+ elif key == "lyrics":
77
+ data[key] = value
78
+ lrc_file = file_path.parent / value
79
+ if not lrc_file.exists():
80
+ warnings.append(f"[{file_name}] lyrics file '{value}' missing.")
81
+
82
+ else:
83
+ errors.append(f"[{file_name}:{line_num}] unidentified metadata found -> '{line}'")
84
+
85
+ if track_identity and not identified:
86
+ title = data.get("romanized-title") or data.get("title")
87
+ artist = data.get("romanized-artist") or data.get("artist")
88
+ if title and artist:
89
+ track_identity(title, artist)
90
+ identified = True
91
+
92
+ missing = REQUIRED - set(data.keys())
93
+ if missing:
94
+ errors.append(f"[{file_name}] missing required parameter(s): {', '.join(missing)}")
95
+
96
+ if errors:
97
+ if not silent:
98
+ print(f"\x1b[1;31m[-] status: FAILED\x1b[0m")
99
+ for error in errors:
100
+ print(f"\x1b[1;31m• [E] {error}\x1b[0m")
101
+ return None
102
+ if not silent:
103
+ print(f"\x1b[1;32m[+] status: PASSED\x1b[0m")
104
+ for warn in warnings:
105
+ print(f"\x1b[1;33m• [!] {warn}\x1b[0m")
106
+
107
+ print("\n\x1b[1;36m[+] parsed metadata:\x1b[0m")
108
+ print(data)
109
+
110
+ return data
111
+
112
+ def _track_identify(file_name: str) -> Any | dict[str, str]:
113
+ directory = Path(__file__).parent.resolve()
114
+ file_path = Path(directory / file_name).resolve()
115
+ file_ext = Path(file_name).suffix
116
+
117
+ if not file_path.exists():
118
+ return
119
+
120
+ if file_ext != ".mdata":
121
+ return
122
+
123
+ track = {}
124
+
125
+ lines = file_path.read_text(encoding="utf-8").splitlines()
126
+
127
+ for line in lines:
128
+ line = line.strip()
129
+
130
+ # discard empty lines
131
+ if not line:
132
+ continue
133
+
134
+ # syntax check // square brackets
135
+ if not (line.startswith("[") and line.endswith("]")) or ":" not in line:
136
+ continue
137
+
138
+ content = line[1:-1].strip()
139
+ key, value = content.split(":", 1)
140
+ key, value = key.strip().lower(), value.strip()
141
+
142
+ if key in ("artist", "romanized-artist"):
143
+ track['artist'] = [a.strip() for a in value.split(",")]
144
+
145
+ elif key in ("title", "romanized-title"):
146
+ track['title'] = value
147
+
148
+ elif key == "explicit":
149
+ if value.lower() not in ("true", "false"):
150
+ continue
151
+ else:
152
+ track['explicit'] = value.lower()
153
+
154
+ else:
155
+ continue
156
+
157
+ return track
158
+
159
+ def multi_file_input():
160
+ files = []
161
+ print("\nenter file names (or hit Enter to finish)")
162
+ while True:
163
+ file = input("\x1b[1;38;2;12;59;5m[?] .mdata file to parse -> ")
164
+ sys.stdout.write("\x1b[0m")
165
+ sys.stdout.flush()
166
+
167
+ if file == "":
168
+ break
169
+
170
+ files.append(file)
171
+
172
+ for mdata_file in files:
173
+ validate_mdata(mdata_file)
174
+
175
+ def single_file_input():
176
+ file = input("\x1b[1;38;2;12;59;5m[?] .mdata file to parse -> ")
177
+ sys.stdout.write("\x1b[0m")
178
+ sys.stdout.flush()
179
+
180
+ validate_mdata(file)
181
+
182
+
183
+ # for systems that have to programmically pass files without user input
184
+ def singlefile(file: str, silent: bool = False) -> dict | None:
185
+ return validate_mdata(file, silent=silent)
186
+
187
+ def multifile(*files: str, silent: bool = False) -> list[dict | None]:
188
+ return [validate_mdata(file, silent=silent) for file in files]
189
+
190
+ def singletrack_identity(file: str) -> None:
191
+ identity = _track_identify(file)
192
+ if not identity:
193
+ return
194
+
195
+ print(f"\x1b[1m[{file}] track info:\x1b[0m")
196
+ print(f"- title: \x1b[1m{identity['title']}\x1b[0m")
197
+ print(f"- artist(s): \x1b[1m{', '.join(identity['artist'])}\x1b[0m")
198
+ print(f"- explicit: \x1b[1m{identity['explicit']}\x1b[0m")
199
+
200
+ def multitrack_identify(*files: str):
201
+ for file in files:
202
+ singletrack_identity(file)
203
+
204
+ def singlefile_autoname(file: str):
205
+ directory = Path(__file__).parent.resolve()
206
+ file_path = Path(directory / file).resolve()
207
+ file_ext = Path(file).suffix
208
+ identity = _track_identify(file)
209
+ if not identity or 'title' not in identity or 'artist' not in identity:
210
+ return
211
+
212
+ title = identity['title']
213
+ artists = ', '.join(identity['artist']) if isinstance(identity['artist'], list) else identity['artist']
214
+
215
+ _temp = re.sub(r"[^\w\s]", "", title)
216
+ filetitle = re.sub(r"\s+", "_", _temp.strip())
217
+
218
+ _temp = re.sub(r"[^\w\s]", "", artists)
219
+ fileartists = re.sub(r"\s+", "_", _temp.strip())
220
+
221
+ fullfile = f"{filetitle.lower()}-{fileartists.lower()}.mdata"
222
+
223
+ new_file = file_path.parent / fullfile
224
+ file_path.replace(new_file)
225
+
226
+ def multifile_autoname(*files: str):
227
+ for file in files:
228
+ singlefile_autoname(file)
229
+
230
+ def directory_mdata(dir_path: str = ".", recursive: bool = False, silent: bool = False) -> list[dict | None]:
231
+ target_dir = Path(__file__).parent.resolve() / dir_path
232
+
233
+ if not target_dir.is_dir():
234
+ if not silent:
235
+ print(f"\x1b[1;31m[-] directory '{dir_path}' not found.\x1b[0m")
236
+ return []
237
+
238
+ # search recursively or just top-level
239
+ pattern = "**/*.mdata" if recursive else "*.mdata"
240
+ mdata_files = [f.name for f in target_dir.glob(pattern)]
241
+
242
+ if not mdata_files and not silent:
243
+ print(f"\x1b[1;33m[!] no .mdata files found in '{dir_path}'.\x1b[0m")
244
+ return []
245
+
246
+ return [validate_mdata(file, silent=silent) for file in mdata_files]
247
+
248
+
249
+ def directory_autoname(dir_path: str = ".", recursive: bool = False):
250
+ target_dir = Path(__file__).parent.resolve() / dir_path
251
+
252
+ if not target_dir.is_dir():
253
+ print(f"\x1b[1;31m[-] directory '{dir_path}' not found.\x1b[0m")
254
+ return
255
+
256
+ pattern = "**/*.mdata" if recursive else "*.mdata"
257
+ for file_path in target_dir.glob(pattern):
258
+ # pass relative path from directory root
259
+ singlefile_autoname(file_path.name)
260
+
261
+ def main():
262
+ while True:
263
+ print("\x1b[1mselect mode:\x1b[0m")
264
+ print("1. single file")
265
+ print("2. multiple files")
266
+ print("3. entire folder")
267
+
268
+ mode = input("\x1b[1;38;2;12;59;5m[?] option (1/2/3) -> \x1b[0m").strip()
269
+
270
+ if mode == "1":
271
+ single_file_input()
272
+ break
273
+ elif mode == "2":
274
+ multi_file_input()
275
+ break
276
+ elif mode == "3":
277
+ folder = input("\x1b[1;38;2;12;59;5m[?] folder path (leave empty for current dir) -> \x1b[0m").strip() or "."
278
+ rec = input("\x1b[1;38;2;12;59;5m[?] include subfolders? (y/n) -> \x1b[0m").lower().startswith("y")
279
+ directory_mdata(folder, recursive=rec)
280
+ break
281
+ else:
282
+ print("\x1b[1;33m[!] invalid option.\x1b[0m\n")
283
+
284
+ if __name__ == "__main__":
285
+ main()
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: music-mdata
3
+ Version: 0.1.0
4
+ Summary: A standard cross-platform parser and metadata builder for .mdata files.
5
+ Author: blasie
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/blasieuwu/music-metadata
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
@@ -0,0 +1,5 @@
1
+ mdata.py,sha256=4VMfh5z1H7cwWuPWTNNUDMUzjaUIauoFyu6WTOcXVOc,9341
2
+ music_mdata-0.1.0.dist-info/METADATA,sha256=BsOKXdnZovLDiIX759ofCFzy7kIwRKlRgJG3pOS4HB0,303
3
+ music_mdata-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
4
+ music_mdata-0.1.0.dist-info/top_level.txt,sha256=h-xO_s81o_AqhGRl5zO5W_Ti8gsxBORLWuQrkZGJ3UY,6
5
+ music_mdata-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ mdata