fetchtune 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.
fetchtune/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from fetchtune.models import Album, Artist, Track
4
+ from fetchtune.resolver import (
5
+ Resolver,
6
+ ResolverError,
7
+ get_default_resolver,
8
+ resolve,
9
+ try_resolve,
10
+ )
11
+
12
+ __all__ = [
13
+ "Album",
14
+ "Artist",
15
+ "Track",
16
+ "Resolver",
17
+ "ResolverError",
18
+ "get_default_resolver",
19
+ "resolve",
20
+ "try_resolve",
21
+ ]
22
+
23
+ __version__ = "0.1.0"
fetchtune/cli.py ADDED
@@ -0,0 +1,202 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from fetchtune import resolve
7
+
8
+
9
+ COPYRIGHT = """
10
+ ────────────────────────────────────────────
11
+ © @momalekiii
12
+ """
13
+
14
+
15
+ def build_parser() -> argparse.ArgumentParser:
16
+ parser = argparse.ArgumentParser(
17
+ prog="fetchtune",
18
+ description="Resolve music links and extract metadata.",
19
+ )
20
+
21
+ parser.add_argument(
22
+ "url",
23
+ nargs="?",
24
+ help="Music URL to resolve.",
25
+ )
26
+
27
+ parser.add_argument(
28
+ "--json",
29
+ action="store_true",
30
+ help="Print the result as JSON.",
31
+ )
32
+
33
+ return parser
34
+
35
+
36
+ def format_duration(
37
+ duration_ms: int | None,
38
+ ) -> str:
39
+ if duration_ms is None:
40
+ return "Unknown"
41
+
42
+ total_seconds = duration_ms // 1000
43
+
44
+ minutes = total_seconds // 60
45
+ seconds = total_seconds % 60
46
+
47
+ return f"{minutes:02d}:{seconds:02d}"
48
+
49
+
50
+ def print_track(track) -> None:
51
+ print()
52
+ print("FetchTune")
53
+ print("─" * 44)
54
+
55
+ print(
56
+ f"Title : {track.title}"
57
+ )
58
+
59
+ artists = ", ".join(
60
+ artist.name
61
+ for artist in track.artists
62
+ )
63
+
64
+ print(
65
+ f"Artists : {artists or 'Unknown'}"
66
+ )
67
+
68
+ if track.album:
69
+ print(
70
+ f"Album : {track.album.name}"
71
+ )
72
+
73
+ if track.album.release_date:
74
+ print(
75
+ f"Release : "
76
+ f"{str(track.album.release_date)[:10]}"
77
+ )
78
+
79
+ if track.album.total_tracks:
80
+ print(
81
+ f"Tracks : "
82
+ f"{track.album.total_tracks}"
83
+ )
84
+ else:
85
+ print("Album : Unknown")
86
+
87
+ print(
88
+ f"Duration : "
89
+ f"{format_duration(track.duration_ms)}"
90
+ )
91
+
92
+ print(
93
+ f"Platform : "
94
+ f"{track.platform}"
95
+ )
96
+
97
+ print(
98
+ f"Explicit : "
99
+ f"{'Yes' if track.is_explicit else 'No'}"
100
+ )
101
+
102
+ if track.platform_id:
103
+ print(
104
+ f"Track ID : "
105
+ f"{track.platform_id}"
106
+ )
107
+
108
+ if track.cover_url:
109
+ print(
110
+ f"Cover : "
111
+ f"{track.cover_url}"
112
+ )
113
+
114
+ if track.url:
115
+ print(
116
+ f"URL : "
117
+ f"{track.url}"
118
+ )
119
+
120
+ print("─" * 44)
121
+ print(COPYRIGHT)
122
+
123
+
124
+ def get_interactive_url() -> str:
125
+ print()
126
+ print("🎵 FetchTune")
127
+ print()
128
+ print("Paste a music link:")
129
+
130
+ try:
131
+ url = input("> ").strip()
132
+ except (EOFError, KeyboardInterrupt):
133
+ print()
134
+ return ""
135
+
136
+ return url
137
+
138
+
139
+ def main(
140
+ argv: list[str] | None = None,
141
+ ) -> int:
142
+ parser = build_parser()
143
+ args = parser.parse_args(argv)
144
+
145
+ # ---------------------------------------------------------
146
+ # URL
147
+ # ---------------------------------------------------------
148
+
149
+ url = args.url
150
+
151
+ if not url:
152
+ url = get_interactive_url()
153
+
154
+ if not url:
155
+ print(
156
+ "FetchTune: no music URL provided.",
157
+ file=sys.stderr,
158
+ )
159
+ return 1
160
+
161
+ # ---------------------------------------------------------
162
+ # Resolve
163
+ # ---------------------------------------------------------
164
+
165
+ try:
166
+ track = resolve(url)
167
+
168
+ except Exception as exc:
169
+ print(
170
+ f"FetchTune error: "
171
+ f"{type(exc).__name__}: {exc}",
172
+ file=sys.stderr,
173
+ )
174
+
175
+ return 1
176
+
177
+ # ---------------------------------------------------------
178
+ # JSON
179
+ # ---------------------------------------------------------
180
+
181
+ if args.json:
182
+ print(
183
+ track.to_json(
184
+ indent=2
185
+ )
186
+ )
187
+
188
+ return 0
189
+
190
+ # ---------------------------------------------------------
191
+ # Normal output
192
+ # ---------------------------------------------------------
193
+
194
+ print_track(track)
195
+
196
+ return 0
197
+
198
+
199
+ if __name__ == "__main__":
200
+ raise SystemExit(
201
+ main()
202
+ )
File without changes
fetchtune/models.py ADDED
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ @dataclass
8
+ class Artist:
9
+ name: str
10
+ id: str | None = None
11
+ url: str | None = None
12
+
13
+ def to_dict(self) -> dict[str, Any]:
14
+ return asdict(self)
15
+
16
+
17
+ @dataclass
18
+ class Album:
19
+ name: str
20
+ id: str | None = None
21
+ url: str | None = None
22
+ cover_url: str | None = None
23
+ release_date: str | None = None
24
+ total_tracks: int | None = None
25
+
26
+ def to_dict(self) -> dict[str, Any]:
27
+ return asdict(self)
28
+
29
+
30
+ @dataclass
31
+ class Track:
32
+ title: str
33
+ artists: list[Artist] = field(default_factory=list)
34
+ album: Album | None = None
35
+
36
+ cover_url: str | None = None
37
+ images: list[dict[str, Any]] = field(default_factory=list)
38
+
39
+ release_date: str | None = None
40
+
41
+ duration_ms: int | None = None
42
+
43
+ is_explicit: bool = False
44
+
45
+ platform: str | None = None
46
+ platform_id: str | None = None
47
+ url: str | None = None
48
+
49
+ def to_dict(self) -> dict[str, Any]:
50
+ return asdict(self)
51
+
52
+ def to_json(self, **kwargs: Any) -> str:
53
+ import json
54
+
55
+ return json.dumps(
56
+ self.to_dict(),
57
+ ensure_ascii=False,
58
+ **kwargs,
59
+ )
60
+
61
+ @property
62
+ def duration_seconds(self) -> float | None:
63
+ if self.duration_ms is None:
64
+ return None
65
+
66
+ return self.duration_ms / 1000
67
+
68
+ @property
69
+ def artist_names(self) -> list[str]:
70
+ return [
71
+ artist.name
72
+ for artist in self.artists
73
+ ]
74
+
75
+ @property
76
+ def artist_string(self) -> str:
77
+ return ", ".join(self.artist_names)
78
+
79
+ def __repr__(self) -> str:
80
+ artists = self.artist_string or "Unknown Artist"
81
+
82
+ return (
83
+ f"Track("
84
+ f"title={self.title!r}, "
85
+ f"artists={artists!r}, "
86
+ f"platform={self.platform!r}"
87
+ f")"
88
+ )
File without changes