Skip to content

Commit

Permalink
Merge pull request #8 from raleighlittles/feature/add-spotify-component
Browse files Browse the repository at this point in the history
Add spotify integration
  • Loading branch information
raleighlittles authored Nov 26, 2024
2 parents 44b6e63 + c5245ba commit 209df9a
Show file tree
Hide file tree
Showing 7 changed files with 369 additions and 0 deletions.
187 changes: 187 additions & 0 deletions spotify_integration/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# Don't commit env file that has API keys
.env
*.json
# Generated by Cargo
# will have compiled files and executables
debug/
target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
9 changes: 9 additions & 0 deletions spotify_integration/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "to_spotify"
version = "0.1.0"
edition = "2021"

[dependencies]
# This must be on a single line for some reason
rspotify = { version = "0.13.3",default-features = false,features = ["env-file", "client-ureq", "ureq-rustls-tls"]}
dotenv = "0.15.0"
45 changes: 45 additions & 0 deletions spotify_integration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# About

Provides API integration with [Spotify](https://en.wikipedia.org/wiki/Spotify)

After running the iTunesDB parser application on an iTunesDB file, you end up with a music CSV file that contains info about your songs. This tool lets you then import that CSV file into a Spotify playlist.

If you want to use this tool with your _own_ CSV files, see `test.csv` for an example of the format.

## Code usage

Install dependencies

```bash
$ pip install requirements.txt
```

```
usage: spotify_integration.py [-h] -f CSV_FILE [-t TRACK_COLUMN] -a API_CREDENTIALS_FILE
Read CSV files containing songs and search for them in Spotify
options:
-h, --help show this help message and exit
-f CSV_FILE, --csv-file CSV_FILE
Path to the CSV file containing the songs
-t TRACK_COLUMN, --track-column TRACK_COLUMN
Name of the column containing the track names
-a API_CREDENTIALS_FILE, --api-credentials-file API_CREDENTIALS_FILE
Path to JSON file containing API credentials, see
spotify_api_credentials.json for the format
```

# API instructions

See `spotify_api_credentials.json` for example of format

Both client_id and client_secret are 32-character alphanumeric values

See: https://developer.spotify.com/documentation/web-api/concepts/apps
for instructions on how to generate!

After running you will be sent to a authorization prompt in your browser and then redirected to a URL

The application will ask you to paste that URL to retrieve the code

7 changes: 7 additions & 0 deletions spotify_integration/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
certifi==2024.8.30
charset-normalizer==3.4.0
idna==3.10
redis==5.2.0
requests==2.32.3
spotipy==2.24.0
urllib3==2.2.3
5 changes: 5 additions & 0 deletions spotify_integration/spotify_api_credentials.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"client_id": "HdtSF4bZRaBBSTISX1oJq0ewYkspqsdB",
"client_secret": "ehZjXinzzmzxPjQ75E7YkvF57uYJstBU",
"redirect_uri": "your_redirect_uri"
}
97 changes: 97 additions & 0 deletions spotify_integration/spotify_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import csv
import spotipy
import argparse
import socket
import json
import datetime

from spotipy.oauth2 import SpotifyClientCredentials
from spotipy.oauth2 import SpotifyOAuth

UNITED_STATES_ISO_3166_1_CODE = "US"
DEFAULT_QUERY_LIMIT = 3
DEFAULT_SPOTIFY_API_SCOPE = "playlist-modify-private"

def get_track_ids_from_csv(csv_file, spotify_api_obj) -> list:

spotify_tracks = list()
num_songs_found = 0

with open(csv_file, mode='r') as csv_file:
csv_reader = csv.DictReader(csv_file)

sp_obj = spotipy.Spotify(auth_manager=SpotifyClientCredentials(
client_id=spotify_api_obj["client_id"], client_secret=spotify_api_obj["client_secret"]))

for csv_row in csv_reader:
song_title = csv_row[argparse_args.track_column]
song_artist = csv_row["Artist"]

if not song_title or not song_artist:
print(f"Skipping row... missing song title or artist")
continue

search_results = sp_obj.search(q=f"track:{song_title} artist:{
song_artist}", type="track", limit=DEFAULT_QUERY_LIMIT, market=UNITED_STATES_ISO_3166_1_CODE)

if search_results["tracks"]["total"] == 0:
print(f"Song '{song_title}' by '{
song_artist}' not found on Spotify!")
continue

else:
num_songs_found += 1
track_id = search_results["tracks"]["items"][0]["id"]
spotify_tracks.append(
dict(song_title=song_title, song_artist=song_artist, track_id=track_id))

print(f"[DEBUG] Found {num_songs_found} songs on Spotify")
return spotify_tracks


def create_playlist_from_tracks(spotify_tracks: list, playlist_name: str, playlist_description: str, spotify_api_obj) -> str:

sp_obj = spotipy.Spotify(auth_manager=SpotifyOAuth(client_id=spotify_api_obj["client_id"], client_secret=spotify_api_obj["client_secret"], redirect_uri=spotify_api_obj["redirect_uri"], scope=DEFAULT_SPOTIFY_API_SCOPE))

user_id = sp_obj.current_user()["id"]
playlist_id = sp_obj.user_playlist_create(
user_id, playlist_name, public=False, description=playlist_description)["id"]

track_ids = [track["track_id"] for track in spotify_tracks]
sp_obj.playlist_add_items(playlist_id, track_ids)

return playlist_id


if __name__ == '__main__':

argparse_parser = argparse.ArgumentParser(
description="Read CSV files containing songs and search for them in Spotify")
argparse_parser.add_argument(
"-f", "--csv-file", help="Path to the CSV file containing the songs", required=True)
argparse_parser.add_argument(
"-t", "--track-column", help="Name of the column containing the track names", required=False, default="Song Title")
argparse_parser.add_argument("-a", "--api-credentials-file",
help="Path to JSON file containing API credentials, see spotify_api_credentials.json for the format", required=True)
argparse_args = argparse_parser.parse_args()

if not argparse_args.csv_file:
raise FileNotFoundError(
f"Error: CSV file '{argparse_args.csv_file}' not found")

if not argparse_args.api_credentials_file:
raise FileNotFoundError(
f"Error: API credentials file '{argparse_args.api_credentials_file}' not found! Go to https://developer.spotify.com/documentation/web-api to create one")

api_obj = json.load(open(argparse_args.api_credentials_file))

spotify_tracks_and_track_ids = get_track_ids_from_csv(
argparse_args.csv_file, api_obj)

# Cannot have newline in playlist description
playlist_description = f"Playlist created on {datetime.datetime.now().isoformat()} Created by {socket.gethostname()}"

new_playlist_id = create_playlist_from_tracks(
spotify_tracks_and_track_ids, "Test Playlist", playlist_description, api_obj)

print(f"Created new playlist with ID {new_playlist_id}")
19 changes: 19 additions & 0 deletions spotify_integration/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use rspotify::clients::BaseClient;
use rspotify::model::SearchType;

fn main() {
println!("Loading credentials from .env file ..");
let creds_from_env_file = rspotify::Credentials::from_env().unwrap();
println!("Credentials loaded! Creating client obj.. ");
let spotify_cli_obj = rspotify::ClientCredsSpotify::new(creds_from_env_file);

// Obtaining the access token - must be done before any query is made
spotify_cli_obj.request_token().unwrap();

let album_query = "album:arrival artist:abba";
let result = spotify_cli_obj.search(album_query, SearchType::Album, None, None, Some(10), None);
match result {
Ok(album) => println!("Searched album: {album:?}"),
Err(err) => println!("Search error! {err:?}"),
}
}

0 comments on commit 209df9a

Please sign in to comment.