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 +23 -0
- fetchtune/cli.py +202 -0
- fetchtune/exceptions.py +0 -0
- fetchtune/models.py +88 -0
- fetchtune/providers/__init__.py +0 -0
- fetchtune/providers/apple.py +623 -0
- fetchtune/providers/base.py +27 -0
- fetchtune/providers/spotify.py +933 -0
- fetchtune/resolver.py +407 -0
- fetchtune-0.1.0.dist-info/METADATA +310 -0
- fetchtune-0.1.0.dist-info/RECORD +15 -0
- fetchtune-0.1.0.dist-info/WHEEL +5 -0
- fetchtune-0.1.0.dist-info/entry_points.txt +2 -0
- fetchtune-0.1.0.dist-info/licenses/LICENSE +21 -0
- fetchtune-0.1.0.dist-info/top_level.txt +1 -0
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
|
+
)
|
fetchtune/exceptions.py
ADDED
|
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
|