stashify-cli 1.3.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.
- stashify/__init__.py +3 -0
- stashify/__main__.py +6 -0
- stashify/cli.py +372 -0
- stashify/config.py +108 -0
- stashify/download.py +104 -0
- stashify/embed.py +271 -0
- stashify/errors.py +21 -0
- stashify/lyrics.py +36 -0
- stashify/models.py +62 -0
- stashify/output.py +59 -0
- stashify/pipeline.py +179 -0
- stashify/sources.py +130 -0
- stashify/spotify.py +256 -0
- stashify/sync.py +93 -0
- stashify/tagger.py +243 -0
- stashify_cli-1.3.0.dist-info/METADATA +488 -0
- stashify_cli-1.3.0.dist-info/RECORD +21 -0
- stashify_cli-1.3.0.dist-info/WHEEL +5 -0
- stashify_cli-1.3.0.dist-info/entry_points.txt +2 -0
- stashify_cli-1.3.0.dist-info/licenses/LICENSE +21 -0
- stashify_cli-1.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stashify-cli
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: Find music from your Spotify library on YouTube and save it locally with cover art, lyrics and full metadata.
|
|
5
|
+
Author: Nicolas Dwi Dharma
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nekonaru/stashify
|
|
8
|
+
Project-URL: Repository, https://github.com/nekonaru/stashify
|
|
9
|
+
Project-URL: Issues, https://github.com/nekonaru/stashify/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/nekonaru/stashify/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Security, https://github.com/nekonaru/stashify/security/advisories
|
|
12
|
+
Keywords: music,spotify,youtube,downloader,cli,spotify-playlist,mp3
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: Natural Language :: English
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: requests>=2.31
|
|
28
|
+
Requires-Dist: yt-dlp>=2024.8.6
|
|
29
|
+
Requires-Dist: mutagen>=1.46
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
<div align="center">
|
|
36
|
+
<img src="https://raw.githubusercontent.com/nekonaru/stashify/main/assets/stashify-mark.png" alt="Stashify logo" width="120" />
|
|
37
|
+
<h1>Stashify: Spotify Music Downloader</h1>
|
|
38
|
+
<p><b>Find any track, album or playlist from Spotify on YouTube and save it locally<br/>
|
|
39
|
+
with cover art, lyrics and full metadata.</b></p>
|
|
40
|
+
|
|
41
|
+
<a href="https://github.com/nekonaru/stashify/releases"><img src="https://img.shields.io/github/v/release/nekonaru/stashify?style=flat-square&color=blue" alt="Release" /></a>
|
|
42
|
+
<a href="https://github.com/nekonaru/stashify/actions/workflows/ci.yml"><img src="https://github.com/nekonaru/stashify/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
|
|
43
|
+
<img src="https://img.shields.io/badge/Python-3.10+-3776AB?style=flat-square&logo=python&logoColor=white" alt="Python" />
|
|
44
|
+
<img src="https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-555555?style=flat-square" alt="Platform" />
|
|
45
|
+
<img src="https://img.shields.io/badge/License-MIT-green?style=flat-square" alt="License" />
|
|
46
|
+
<img src="https://img.shields.io/badge/Status-Active-brightgreen?style=flat-square" alt="Status" />
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<br />
|
|
50
|
+
|
|
51
|
+
**English** | [Bahasa Indonesia](https://github.com/nekonaru/stashify/blob/main/README.id.md)
|
|
52
|
+
|
|
53
|
+
> [!NOTE]
|
|
54
|
+
> Stashify reads public Spotify pages, no account or login required. It can only see what is public: tracks, albums, artists and public playlists.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Table of Contents
|
|
59
|
+
|
|
60
|
+
- [About](#about)
|
|
61
|
+
- [Features](#features)
|
|
62
|
+
- [Requirements](#requirements)
|
|
63
|
+
- [Installation](#installation)
|
|
64
|
+
- [Usage](#usage)
|
|
65
|
+
- [Configuration](#configuration)
|
|
66
|
+
- [Data Sources](#data-sources)
|
|
67
|
+
- [Project Structure](#project-structure)
|
|
68
|
+
- [Development](#development)
|
|
69
|
+
- [Testing](#testing)
|
|
70
|
+
- [Continuous Integration](#continuous-integration)
|
|
71
|
+
- [FAQ](#faq)
|
|
72
|
+
- [Author](#author)
|
|
73
|
+
- [Disclaimer](#disclaimer)
|
|
74
|
+
- [License](#license)
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## About
|
|
79
|
+
|
|
80
|
+
**Stashify** is a command line tool that turns a Spotify link into a properly tagged audio file on your machine. It resolves the link against Spotify's public pages, finds the best matching source for each track on YouTube, converts it with FFmpeg and tags it with title, artists, album, track number, release year, cover art and lyrics.
|
|
81
|
+
|
|
82
|
+
> _"Your Spotify library, stashed as real files."_
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Features
|
|
87
|
+
|
|
88
|
+
### Download
|
|
89
|
+
|
|
90
|
+
| Feature | Description |
|
|
91
|
+
|---------|-------------|
|
|
92
|
+
| **Track, album, playlist, artist** | Public Spotify links resolve to a track list and every track downloads (artist links fetch the artist's top 10 tracks, public playlists show up to 50 tracks) |
|
|
93
|
+
| **No account needed** | Metadata comes from Spotify's public embed pages, nothing to create or configure |
|
|
94
|
+
| **Best-match source** | YouTube results are scored on duration and word overlap, the strongest match wins |
|
|
95
|
+
| **Audio formats** | MP3, M4A, Opus, FLAC or WAV, converted through FFmpeg |
|
|
96
|
+
| **Bitrate control** | Choose the bitrate for lossy formats, default `192k` |
|
|
97
|
+
| **Skip existing** | Files that already exist are skipped, `--overwrite` forces a re-download |
|
|
98
|
+
| **Parallel downloads** | N tracks at a time with `--jobs` (default 2, max 8), faster on large playlists |
|
|
99
|
+
| **Dry run** | `--dry-run` prints the matched source per track without downloading anything |
|
|
100
|
+
|
|
101
|
+
### Metadata
|
|
102
|
+
|
|
103
|
+
| Feature | Description |
|
|
104
|
+
|---------|-------------|
|
|
105
|
+
| **Full tagging** | Title, artists, album (when known), album artist (when known), track number (albums), disc numbers, release year, cover art |
|
|
106
|
+
| **Cover art** | Original cover embedded, per track for playlists and artists, album cover for albums |
|
|
107
|
+
| **Lyrics** | Plain-text lyrics fetched from a community lyrics API and embedded when found |
|
|
108
|
+
| **Retag existing files** | `stashify meta` writes the same metadata into files you already have |
|
|
109
|
+
| **Path templates** | `{artist} - {title}` style templates, subfolders included, safe for any OS |
|
|
110
|
+
| **Save as JSON** | `stashify save` dumps the resolved metadata without downloading audio |
|
|
111
|
+
|
|
112
|
+
### Sync
|
|
113
|
+
|
|
114
|
+
| Feature | Description |
|
|
115
|
+
|---------|-------------|
|
|
116
|
+
| **Playlist sync** | `stashify sync` keeps a folder in sync with a playlist: new songs are downloaded, removed songs are deleted |
|
|
117
|
+
| **Self-contained state** | A small `stashify.sync.json` state file tracks which files Stashify downloaded, it never touches files it did not create |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Requirements
|
|
122
|
+
|
|
123
|
+
| Dependency | Minimum Version | Notes |
|
|
124
|
+
|------------|-----------------|-------|
|
|
125
|
+
| **Python** | 3.10 | Required |
|
|
126
|
+
| **FFmpeg** | 6 | Required for audio conversion |
|
|
127
|
+
|
|
128
|
+
> [!TIP]
|
|
129
|
+
> No Spotify account is required. Only public content (tracks, albums, artists and public playlists) can be resolved; private playlists are not reachable without an official API entitlement.
|
|
130
|
+
|
|
131
|
+
### Never used a terminal? Start here
|
|
132
|
+
|
|
133
|
+
<details>
|
|
134
|
+
<summary><b>How to open a terminal on your OS</b></summary>
|
|
135
|
+
|
|
136
|
+
**Windows**
|
|
137
|
+
|
|
138
|
+
1. Press the **Windows** key, type `PowerShell`, then press **Enter**
|
|
139
|
+
2. A blue window opens. That is where you type commands
|
|
140
|
+
|
|
141
|
+
**macOS**
|
|
142
|
+
|
|
143
|
+
1. Press **Command** + **Space**, type `Terminal`, then press **Enter**
|
|
144
|
+
2. A window with a prompt opens
|
|
145
|
+
|
|
146
|
+
**Linux**
|
|
147
|
+
|
|
148
|
+
1. Open your app launcher and type `terminal`, then press **Enter**
|
|
149
|
+
2. A window with a prompt opens
|
|
150
|
+
|
|
151
|
+
Every `stashify ...` command in this guide is typed in that window, followed by **Enter**.
|
|
152
|
+
|
|
153
|
+
</details>
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Installation
|
|
158
|
+
|
|
159
|
+
### Option A: Install from PyPI (easiest)
|
|
160
|
+
|
|
161
|
+
1. Make sure Python 3.10 or newer is installed (`python --version` should answer)
|
|
162
|
+
2. Make sure FFmpeg is installed (`ffmpeg -version` should answer). If not:
|
|
163
|
+
- Windows: download from [ffmpeg.org](https://ffmpeg.org/download.html) or `winget install ffmpeg`
|
|
164
|
+
- macOS: `brew install ffmpeg`
|
|
165
|
+
- Linux: `sudo apt install ffmpeg` (or your distribution's package manager)
|
|
166
|
+
3. Install Stashify from PyPI:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
pip install stashify-cli
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
4. Verify the install:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
stashify --version
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
> [!NOTE]
|
|
179
|
+
> The PyPI package is `stashify-cli`; the command it installs is `stashify`. If pip cannot find the package on PyPI yet, use Option B.
|
|
180
|
+
|
|
181
|
+
### Option B: Install directly from GitHub
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
pip install git+https://github.com/nekonaru/stashify.git
|
|
185
|
+
stashify --version
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Needs Python 3.10+ and FFmpeg, same as above.
|
|
189
|
+
|
|
190
|
+
### Option C: Build from source (for development / to read or change the code)
|
|
191
|
+
|
|
192
|
+
#### 1. Get the project code
|
|
193
|
+
|
|
194
|
+
<details>
|
|
195
|
+
<summary><b>I have Git installed</b></summary>
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
git clone https://github.com/nekonaru/stashify.git
|
|
199
|
+
cd stashify
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
</details>
|
|
203
|
+
|
|
204
|
+
<details>
|
|
205
|
+
<summary><b>I don't have Git</b></summary>
|
|
206
|
+
|
|
207
|
+
1. Open the repository page on GitHub
|
|
208
|
+
2. Click the green **`Code`** button, choose **`Download ZIP`**
|
|
209
|
+
3. Extract the ZIP to a folder of your choice
|
|
210
|
+
4. In the terminal, go into the extracted folder, for example:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
cd Downloads/stashify-main
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
</details>
|
|
217
|
+
|
|
218
|
+
#### 2. Install the dependencies and verify
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
python -m pip install -e .[dev]
|
|
222
|
+
python -m pytest -q
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
> [!IMPORTANT]
|
|
226
|
+
> The test suite must pass before you build or publish anything.
|
|
227
|
+
|
|
228
|
+
#### 3. Build and check
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
python -m ruff check src tests
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Stashify is a pure Python package. There is no binary to compile; installation is the packaging step.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Usage
|
|
239
|
+
|
|
240
|
+
### First launch
|
|
241
|
+
|
|
242
|
+
Stashify needs no account, no login and no API keys. Install it, then download:
|
|
243
|
+
|
|
244
|
+
1. Find any Spotify link for a track, album, playlist or artist (public content only)
|
|
245
|
+
2. Run:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
stashify download "https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M"
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
> [!NOTE]
|
|
252
|
+
> The download works for public content only. Private playlists are not reachable: Spotify requires an official API entitlement for that, which Development Mode apps no longer get.
|
|
253
|
+
|
|
254
|
+
### Everyday use
|
|
255
|
+
|
|
256
|
+
- Download a single track, an album, a playlist or an artist's top tracks by passing their links to `stashify download`; files land in your Downloads folder by default
|
|
257
|
+
- Pick the audio format with `-f mp3|m4a|opus|flac|wav` (default `mp3`)
|
|
258
|
+
- Change the output folder with `-o "D:\Music"` and the naming scheme with `-t "{album}/{track_number} {title}"`
|
|
259
|
+
- Download up to 8 tracks at once with `--jobs 4` (default 2)
|
|
260
|
+
- Skip cover art or lyrics with `--no-cover` / `--no-lyrics`, or re-fetch everything with `--overwrite`
|
|
261
|
+
- Preview the match without downloading with `--dry-run`
|
|
262
|
+
- Keep a folder in sync with a playlist: `stashify sync "PLAYLIST_URL" -o "D:\Music\MyList"`
|
|
263
|
+
- Retag files you already have: `stashify meta file1.mp3 file2.mp3 --url "ALBUM_URL"`
|
|
264
|
+
- Export the resolved metadata as JSON: `stashify save "ALBUM_URL" --save-file album.json`
|
|
265
|
+
- Print the source URL that would be used per track: `stashify url "PLAYLIST_URL"`
|
|
266
|
+
|
|
267
|
+
The path template supports `{artist}`, `{artists}`, `{title}`, `{album}`, `{album_artist}`, `{track_number}`, `{disc_number}` and `{release_year}`. Use `/` to create subfolders. Illegal file name characters are removed automatically.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Configuration
|
|
272
|
+
|
|
273
|
+
Preferences can be stored in a config file; secrets cannot. The default provider needs none.
|
|
274
|
+
|
|
275
|
+
Config file locations: `%APPDATA%\stashify\config.json` (Windows), `~/.config/stashify/config.json` (macOS and Linux).
|
|
276
|
+
|
|
277
|
+
| Key | Type | Default | Effect |
|
|
278
|
+
|-----|------|---------|--------|
|
|
279
|
+
| `output_dir` | string | your Downloads folder | Output directory for downloads (Windows: `%USERPROFILE%\Downloads`, macOS and Linux: `~/Downloads` or `XDG_DOWNLOAD_DIR`) |
|
|
280
|
+
| `jobs` | integer | `2` | Parallel downloads, between 1 and 8 |
|
|
281
|
+
| `audio_format` | string | `mp3` | Default audio format |
|
|
282
|
+
| `bitrate` | string | `192k` | Bitrate for lossy formats |
|
|
283
|
+
| `template` | string | `{artist} - {title}` | Output path template |
|
|
284
|
+
| `market` | string | `US` | Two-letter market code (only used with `--provider api`) |
|
|
285
|
+
| `provider` | string | `embed` | `embed` (default, no account) or `api` (official Web API, needs a Premium-entitled developer app) |
|
|
286
|
+
| `detailed` | boolean | `true` | Fetch per-track details (artists, cover, release year) for playlists |
|
|
287
|
+
| `overwrite` | boolean | `false` | Re-download files that already exist |
|
|
288
|
+
| `no_cover` | boolean | `false` | Never embed cover art |
|
|
289
|
+
| `no_lyrics` | boolean | `false` | Never embed lyrics |
|
|
290
|
+
|
|
291
|
+
Command line flags always win over the config file. The default output directory is your Downloads folder; set the `STASHIFY_DOWNLOAD_DIR` environment variable, the `-o` flag or the `output_dir` key to change it.
|
|
292
|
+
|
|
293
|
+
Use `--provider api` with `--client-id` and `--client-secret` (or `SPOTIFY_CLIENT_ID` / `SPOTIFY_CLIENT_SECRET` environment variables) if you have an official Web API entitlement; it adds ISRC and album metadata for tracks and albums.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Data Sources
|
|
298
|
+
|
|
299
|
+
Stashify is online only when it resolves metadata, searches for a source or fetches lyrics. Audio is sourced from YouTube.
|
|
300
|
+
|
|
301
|
+
| Purpose | Source |
|
|
302
|
+
|---------|--------|
|
|
303
|
+
| Track, album, playlist and artist metadata | Spotify's public embed pages (default) or the Spotify Web API (`--provider api`) |
|
|
304
|
+
| Cover art | Spotify image CDN (embedded into files) |
|
|
305
|
+
| Audio source | YouTube, through yt-dlp |
|
|
306
|
+
| Lyrics | lyrics.ovh (no key required) |
|
|
307
|
+
|
|
308
|
+
> [!NOTE]
|
|
309
|
+
> Some tracks have no usable match on YouTube. Stashify reports the best candidate it found and skips tracks whose score is too low instead of downloading the wrong song.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Project Structure
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
stashify/
|
|
317
|
+
├── src/
|
|
318
|
+
│ └── stashify/
|
|
319
|
+
│ ├── __init__.py # package version
|
|
320
|
+
│ ├── __main__.py # python -m stashify entry point
|
|
321
|
+
│ ├── cli.py # argument parsing and commands
|
|
322
|
+
│ ├── pipeline.py # shared download pipeline
|
|
323
|
+
│ ├── spotify.py # Spotify Web API client (optional, --provider api)
|
|
324
|
+
│ ├── embed.py # public embed page resolver (default provider)
|
|
325
|
+
│ ├── models.py # Track data model
|
|
326
|
+
│ ├── sources.py # YouTube search and scoring
|
|
327
|
+
│ ├── download.py # yt-dlp download and FFmpeg conversion
|
|
328
|
+
│ ├── tagger.py # MP3, M4A, FLAC, Opus and WAV tagging
|
|
329
|
+
│ ├── lyrics.py # lyrics.ovh client
|
|
330
|
+
│ ├── output.py # path templates and file naming
|
|
331
|
+
│ ├── sync.py # playlist sync state handling
|
|
332
|
+
│ ├── config.py # config file loading
|
|
333
|
+
│ └── errors.py # error types
|
|
334
|
+
├── tests/ # pytest suite (offline, network mocked)
|
|
335
|
+
├── assets/ # logo and icons
|
|
336
|
+
├── .github/
|
|
337
|
+
│ └── workflows/
|
|
338
|
+
│ ├── ci.yml # lint and tests on every push
|
|
339
|
+
│ ├── publish.yml # publish to PyPI on demand (OIDC)
|
|
340
|
+
│ └── release.yml # draft release from changelog on tags
|
|
341
|
+
├── pyproject.toml # packaging and tool configuration
|
|
342
|
+
├── SECURITY.md # vulnerability reporting policy
|
|
343
|
+
├── LICENSE
|
|
344
|
+
├── README.md # main documentation (English)
|
|
345
|
+
└── README.id.md # documentation (Bahasa Indonesia)
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Development
|
|
351
|
+
|
|
352
|
+
### Publishing to PyPI
|
|
353
|
+
|
|
354
|
+
One-time setup, done by the maintainer on PyPI:
|
|
355
|
+
|
|
356
|
+
1. Open [pypi.org](https://pypi.org), sign in, go to **Account → Publishing**
|
|
357
|
+
2. Add a pending publisher with these exact values:
|
|
358
|
+
- Project name: `stashify-cli`
|
|
359
|
+
- Owner: `nekonaru`
|
|
360
|
+
- Repository: `stashify`
|
|
361
|
+
- Workflow name: `publish.yml`
|
|
362
|
+
- Environment name: leave empty
|
|
363
|
+
3. Push a release tag, then run **Publish** from the Actions tab on the tag you want to release
|
|
364
|
+
|
|
365
|
+
The workflow builds the wheel and sdist, checks them with `twine check`, smoke tests the wheel and publishes via OIDC. No API token is stored anywhere.
|
|
366
|
+
|
|
367
|
+
> [!WARNING]
|
|
368
|
+
> Stashify is distributed on PyPI as `stashify-cli` because the name `stashify` already belongs to someone else. Do not try to claim it.
|
|
369
|
+
|
|
370
|
+
### Working on the code
|
|
371
|
+
|
|
372
|
+
1. Install the package in editable mode with the dev extras: `python -m pip install -e .[dev]`
|
|
373
|
+
2. Make your change
|
|
374
|
+
3. Run the checks: `python -m ruff check src tests` and `python -m pytest -q`
|
|
375
|
+
|
|
376
|
+
### Adding a new supported audio format
|
|
377
|
+
|
|
378
|
+
1. Add the format to `AUDIO_FORMATS` in `src/stashify/download.py` and to `_CODEC_MAP`
|
|
379
|
+
2. Add a tagging path in `src/stashify/tagger.py` (see `_tag_flac` for an example)
|
|
380
|
+
3. Add a test that renders and retags a short file with FFmpeg, see `tests/test_tagger.py`
|
|
381
|
+
|
|
382
|
+
> [!WARNING]
|
|
383
|
+
> `assets/` is committed. Do not commit downloaded audio, cover art from real releases, or your config file. Anything that looks like personal media stays out of the repository.
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Testing
|
|
388
|
+
|
|
389
|
+
Verification before every push, both must be clean:
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
python -m ruff check src tests
|
|
393
|
+
python -m pytest -q
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
| Check | Requirement |
|
|
397
|
+
|-------|-------------|
|
|
398
|
+
| `python -m ruff check src tests` | Must report **All checks passed** (zero errors, zero warnings) |
|
|
399
|
+
| `python -m pytest -q` | Must pass the full suite |
|
|
400
|
+
|
|
401
|
+
Lint rules are enforced through the `[tool.ruff]` section in `pyproject.toml`.
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Continuous Integration
|
|
406
|
+
|
|
407
|
+
| Workflow | Purpose |
|
|
408
|
+
|----------|---------|
|
|
409
|
+
| `ci.yml` | Ruff and the test suite on Python 3.10 and 3.12, on every push and pull request |
|
|
410
|
+
| `release.yml` | Test suite plus a draft GitHub release built from the changelogs, on tags `v*` |
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## FAQ
|
|
415
|
+
|
|
416
|
+
<details>
|
|
417
|
+
<summary><b>Do I need a Spotify account, login or developer app?</b></summary>
|
|
418
|
+
|
|
419
|
+
No. The default provider reads Spotify's public pages, exactly what a browser sees without logging in. Nothing to create, nothing to log into, nothing to leak.
|
|
420
|
+
|
|
421
|
+
</details>
|
|
422
|
+
|
|
423
|
+
<details>
|
|
424
|
+
<summary><b>Why does my private playlist fail?</b></summary>
|
|
425
|
+
|
|
426
|
+
Public pages only exist for public content. If the playlist is private, the embed page shows a not-found page and Stashify reports it. Making the playlist public fixes it. Alternatively, official API access to your own playlists requires a Premium-entitled developer app, then `stashify ... --provider api` with your own credentials.
|
|
427
|
+
|
|
428
|
+
</details>
|
|
429
|
+
|
|
430
|
+
<details>
|
|
431
|
+
<summary><b>Why is some metadata missing (ISRC, album name)?</b></summary>
|
|
432
|
+
|
|
433
|
+
Public pages do not carry every field. Stashify tags what is available: title, artists, duration, release year, cover art, album name and track numbers for albums. ISRC is only available through the official Web API.
|
|
434
|
+
|
|
435
|
+
</details>
|
|
436
|
+
|
|
437
|
+
<details>
|
|
438
|
+
<summary><b>Some songs are skipped or fail to download. Why?</b></summary>
|
|
439
|
+
|
|
440
|
+
Either there is no usable match on YouTube (unreleased tracks, very obscure remixes, private uploads) or yt-dlp needs an update, because YouTube changes frequently. Run `pip install --upgrade yt-dlp` and try again.
|
|
441
|
+
|
|
442
|
+
</details>
|
|
443
|
+
|
|
444
|
+
<details>
|
|
445
|
+
<summary><b>The wrong version of a song gets downloaded. What can I do?</b></summary>
|
|
446
|
+
|
|
447
|
+
The scorer strongly prefers duration-matched audio with "official" in the title and rejects karaoke, covers and live versions unless you ask for them. Use `--dry-run` first; if the match keeps coming out wrong, open an issue with the Spotify link.
|
|
448
|
+
|
|
449
|
+
</details>
|
|
450
|
+
|
|
451
|
+
<details>
|
|
452
|
+
<summary><b>A file already exists. Will it be overwritten?</b></summary>
|
|
453
|
+
|
|
454
|
+
No. Existing files are skipped. Pass `--overwrite` to re-download them.
|
|
455
|
+
|
|
456
|
+
</details>
|
|
457
|
+
|
|
458
|
+
<details>
|
|
459
|
+
<summary><b>Can I keep my library folder in sync with a Spotify playlist?</b></summary>
|
|
460
|
+
|
|
461
|
+
Yes. `stashify sync "PLAYLIST_URL" -o "D:\Music\MyList"` downloads new tracks and deletes only the files that Stashify downloaded and that have since disappeared from the playlist. Everything else in the folder is untouched.
|
|
462
|
+
|
|
463
|
+
</details>
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Author
|
|
468
|
+
|
|
469
|
+
<div align="center">
|
|
470
|
+
|
|
471
|
+
| [](https://github.com/nekonaru) |
|
|
472
|
+
|:---:|
|
|
473
|
+
| **Nicolas Dwi Dharma** |
|
|
474
|
+
| [github.com/nekonaru](https://github.com/nekonaru) |
|
|
475
|
+
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## Disclaimer
|
|
481
|
+
|
|
482
|
+
Stashify is an independent open-source project and is not affiliated with or endorsed by Spotify, Google, YouTube or the operators of lyrics.ovh. The default provider parses Spotify's public embed pages; this is not a documented API and can change or break at any time. Downloading music may violate the terms of service of those platforms and may infringe copyright in your jurisdiction. Stashify is provided for personal, lawful use only; you are responsible for how you use it.
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## License
|
|
487
|
+
|
|
488
|
+
Distributed under the **MIT License**. See [`LICENSE`](https://github.com/nekonaru/stashify/blob/main/LICENSE) for more information.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
stashify/__init__.py,sha256=1qJ_PQC0qFXywMbLlh5_h-82iwwqopjxyTeBjSZxN-Y,108
|
|
2
|
+
stashify/__main__.py,sha256=4JMK66Wj4uLZTKbF-sT3LAxOsr6buig77PmOkJCRRxw,83
|
|
3
|
+
stashify/cli.py,sha256=G8vfGsA3kkOwGYi8sihKu0JkR1TG16p5ATtpB4tZ1E4,12981
|
|
4
|
+
stashify/config.py,sha256=m4wc3WjpJFRuVpcHG9BNlWJfRsUN24U42z3aRSsnuYs,3455
|
|
5
|
+
stashify/download.py,sha256=LIa9-QdMVI8tTejKzpA66-Tz-kKcoolUmNTdvCAl_1o,3028
|
|
6
|
+
stashify/embed.py,sha256=uOA-PAproiTfCyQFJf9bo6Yxl1_fJgpx6HwGS27iSKU,10021
|
|
7
|
+
stashify/errors.py,sha256=K5WOZ5AKy1OdQehTuL2r7V488h4lkrbxAnp6fXJ4tiU,517
|
|
8
|
+
stashify/lyrics.py,sha256=Fw18Ti2fP4XmbK3-MijoE8zr-AMUqkGI1bzKmFuJUys,937
|
|
9
|
+
stashify/models.py,sha256=pN36Ak8Ch57AwuI_-kFUjF8n86AkhlgyPgyA6BcTNnE,1945
|
|
10
|
+
stashify/output.py,sha256=IXEpWrHwYqyAyDWy7e8txYEbEcBW1Ud-EflACPxdM7w,1760
|
|
11
|
+
stashify/pipeline.py,sha256=aCJBatWImwtdTZsvLC0SO9kMnhwU6EochqJI15fQPNs,5488
|
|
12
|
+
stashify/sources.py,sha256=QpKlfCWhi5Cka2Ii8kAHOwr8pBBiEkCfmvS8xTy8zSQ,4125
|
|
13
|
+
stashify/spotify.py,sha256=FG67_HQnsrefv4Tlu1RHGgza5nUc8tbEfSPJzKBC21Q,9749
|
|
14
|
+
stashify/sync.py,sha256=RSTOrfqMIhcshByetwdBGt_F8IabBvcA77I3VWhCDA0,3175
|
|
15
|
+
stashify/tagger.py,sha256=tKBO10traw3X5MrW3hxlQZnFvFw3sWcRnCBEF8u_nTo,7752
|
|
16
|
+
stashify_cli-1.3.0.dist-info/licenses/LICENSE,sha256=uobiDfGzhjZ0X4EtAnLhm12ucARmGbJoS3VsI9DB8aw,1075
|
|
17
|
+
stashify_cli-1.3.0.dist-info/METADATA,sha256=I6KMf_RUJajob9Dj5-LihPAgMEfkUDy1eMex-DP3VPY,19717
|
|
18
|
+
stashify_cli-1.3.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
19
|
+
stashify_cli-1.3.0.dist-info/entry_points.txt,sha256=8m69hKT79T8fxNXx23zjrJ9mMGJy7w102NDRtYZTozg,47
|
|
20
|
+
stashify_cli-1.3.0.dist-info/top_level.txt,sha256=0WWac7j_prOd0Md8GLHR0x52j824M0ED3k0qrQL4hWM,9
|
|
21
|
+
stashify_cli-1.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nicolas Dwi Dharma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
stashify
|