Skip to content

Commit 2dc0a74

Browse files
authored
Merge pull request #47 from CalPinSW/add-database-to-project
Add database to project
2 parents ddc4b71 + a6493d2 commit 2dc0a74

40 files changed

+854
-25
lines changed

ansible/.ansible-secrets.template.yml

+3
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ spotify_client_id: spotify_client_id
22
spotify_secret: spotify_secret
33
debug_mode: false
44
flask_secret_key: flask_secret_key
5+
musicbrainz_url: musicbrainz_url
6+
musicbrainz_user_agent: musicbrainz_user_agent
7+
db_connection_string: db_connection_string

backend/.env.j2

+7
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,10 @@ SECRET_KEY={{flask_secret_key}}
1414
SPOTIFY_CLIENT_ID={{spotify_client_id}}
1515
SPOTIFY_SECRET={{spotify_secret}}
1616
SPOTIFY_REDIRECT_URI="{{backend_url}}/auth/get-user-code"
17+
18+
# Musicbrainz Api
19+
MUSICBRAINZ_URL={{musicbrainz_url}}
20+
MUSICBRAINZ_USER_AGENT={{musicbrainz_user_agent}}
21+
22+
# Database Connection String
23+
DB_CONNECTION_STRING={{db_connection_string}}

backend/.env.template

+7
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,10 @@ SECRET_KEY="secret-key"
1414
SPOTIFY_CLIENT_ID="https://developer.spotify.com/dashboard"
1515
SPOTIFY_SECRET="https://developer.spotify.com/dashboard"
1616
SPOTIFY_REDIRECT_URI="http://localhost:8080/spotify-redirect"
17+
18+
# Musicbrainz Api
19+
MUSICBRAINZ_URL=https://musicbrainz.org/ws/2
20+
MUSICBRAINZ_USER_AGENT="Application PlaylistManager/1.0 - Hobby project (put maintainers email here)"
21+
22+
# Database Connection String
23+
DB_CONNECTION_STRING=postgresql://username:password@hostname:port/database

backend/.env.test

+7
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,10 @@ SECRET_KEY="secret-key"
1414
SPOTIFY_CLIENT_ID="https://developer.spotify.com/dashboard"
1515
SPOTIFY_SECRET="https://developer.spotify.com/dashboard"
1616
SPOTIFY_REDIRECT_URI="http://localhost:8080/spotify-redirect"
17+
18+
# Musicbrainz Api
19+
MUSICBRAINZ_URL=https://musicbrainz.org/ws/2
20+
MUSICBRAINZ_USER_AGENT="Application PlaylistManager/1.0 - Hobby project (put maintainers email here)"
21+
22+
# Database Connection String
23+
DB_CONNECTION_STRING=postgresql://username:password@hostname:port/database

backend/README.md

+8
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,11 @@ If you now run [the frontend](../frontend/README.md) you should be able to commu
6262
## Running Tests
6363

6464
Tests can be run with `poetry run pytest`
65+
66+
67+
## Running DB Migrations
68+
69+
Load env variables with `source .env`
70+
Run Python locally `python3`
71+
Import migration file >>>: `from src.database.migrations.(migration_file_name) import up, down`
72+
Run migration >>>: `up()` / `down()`

backend/poetry.lock

+49-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/pyproject.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ packages = [
1313
python = "^3.10"
1414
flask = "^3.0.3"
1515
python-dotenv = "^1.0.0"
16-
flask-cors = "^4.0.1"
16+
flask-cors = "^5.0.0"
1717
pydantic = "^2.6.4"
1818
requests = "^2.32.3"
1919
gunicorn = "^22.0.0"
20+
peewee = "^3.17.6"
21+
asyncio = "^3.4.3"
22+
psycopg2 = "^2.9.9"
2023

2124
[tool.poetry.group.dev.dependencies]
2225
pytest = "^8.1.1"

backend/src/app.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
from flask import Flask, make_response
22
from flask_cors import CORS
3+
from src.controllers.database import database_controller
34
from src.controllers.spotify import spotify_controller
45
from src.exceptions.Unauthorized import UnauthorizedException
56
from src.flask_config import Config
7+
from src.musicbrainz import MusicbrainzClient
68
from src.spotify import SpotifyClient
79
from src.controllers.auth import auth_controller
810

911

1012
def create_app():
1113
app = Flask(__name__)
1214
spotify = SpotifyClient()
15+
musicbrainz = MusicbrainzClient()
16+
1317
app.config.from_object(Config())
1418
app.config["CORS_HEADERS"] = "Content-Type"
1519

@@ -21,7 +25,7 @@ def create_app():
2125
SESSION_COOKIE_SECURE="True",
2226
)
2327

24-
cors = CORS(
28+
CORS(
2529
app,
2630
resources={
2731
r"/*": {
@@ -44,4 +48,8 @@ def handle_unauthorized_exception(_):
4448

4549
app.register_blueprint(auth_controller(spotify=spotify))
4650
app.register_blueprint(spotify_controller(spotify=spotify))
51+
app.register_blueprint(
52+
database_controller(spotify=spotify, musicbrainz=musicbrainz)
53+
)
54+
4755
return app

backend/src/controllers/database.py

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
from flask import Blueprint, make_response, request
2+
3+
from src.database.crud.album import (
4+
add_genres_to_album,
5+
get_album_artists,
6+
get_album_genres,
7+
get_user_albums,
8+
update_album,
9+
)
10+
from src.database.crud.genre import create_genre
11+
from src.database.crud.playlist import (
12+
create_playlist,
13+
get_playlist_albums,
14+
get_playlist_by_id_or_none,
15+
get_user_playlists,
16+
update_playlist,
17+
)
18+
from src.database.crud.user import get_or_create_user
19+
from src.musicbrainz import MusicbrainzClient
20+
from src.spotify import SpotifyClient
21+
from time import sleep
22+
23+
24+
def database_controller(spotify: SpotifyClient, musicbrainz: MusicbrainzClient):
25+
database_controller = Blueprint(
26+
name="database_controller", import_name=__name__, url_prefix="/database"
27+
)
28+
29+
@database_controller.route("populate_user", methods=["GET"])
30+
def populate_user():
31+
access_token = request.cookies.get("spotify_access_token")
32+
user = spotify.get_current_user(access_token)
33+
simplified_playlists = spotify.get_all_playlists(
34+
user_id=user.id, access_token=access_token
35+
)
36+
(db_user,) = get_or_create_user(user)
37+
38+
for simplified_playlist in simplified_playlists:
39+
if "Albums" in simplified_playlist.name:
40+
db_playlist = get_playlist_by_id_or_none(simplified_playlist.id)
41+
42+
if db_playlist is None:
43+
[playlist, albums] = [
44+
spotify.get_playlist(
45+
access_token=access_token, id=simplified_playlist.id
46+
),
47+
spotify.get_playlist_album_info(
48+
access_token=access_token, id=simplified_playlist.id
49+
),
50+
]
51+
create_playlist(playlist, albums, db_user)
52+
else:
53+
if db_playlist.snapshot_id != simplified_playlist.snapshot_id:
54+
[playlist, albums] = [
55+
spotify.get_playlist(
56+
access_token=access_token, id=simplified_playlist.id
57+
),
58+
spotify.get_playlist_album_info(
59+
access_token=access_token, id=simplified_playlist.id
60+
),
61+
]
62+
update_playlist(playlist, albums)
63+
64+
return make_response("Playlist data populated", 201)
65+
66+
@database_controller.route(
67+
"populate_additional_album_details_from_playlist", methods=["GET"]
68+
)
69+
def populate_additional_album_details_from_playlist():
70+
access_token = request.cookies.get("spotify_access_token")
71+
user = spotify.get_current_user(access_token)
72+
playlists = get_user_playlists(user.id)
73+
74+
for playlist in playlists:
75+
albums = get_playlist_albums(playlist.id)
76+
if albums == []:
77+
continue
78+
batch_albums = split_list(albums, 20)
79+
for album_chunk in batch_albums:
80+
sleep(0.5)
81+
albums = spotify.get_multiple_albums(
82+
access_token=access_token, ids=[album.id for album in album_chunk]
83+
)
84+
for db_album in albums:
85+
album = spotify.get_album(access_token=access_token, id=db_album.id)
86+
update_album(album)
87+
88+
return make_response("Playlist data populated", 201)
89+
90+
@database_controller.route("populate_additional_album_details", methods=["GET"])
91+
def populate_additional_album_details():
92+
access_token = request.cookies.get("spotify_access_token")
93+
user = spotify.get_current_user(access_token)
94+
albums = get_user_albums(user.id)
95+
albums_without_label = [album for album in albums] # if album.label is None]
96+
if albums_without_label == []:
97+
return make_response("No Albums to process", 204)
98+
batch_albums = split_list(albums_without_label, 20)
99+
for album_chunk in batch_albums:
100+
sleep(0.5)
101+
albums = spotify.get_multiple_albums(
102+
access_token=access_token, ids=[album.id for album in album_chunk]
103+
)
104+
for db_album in albums:
105+
db_album.genres = musicbrainz.get_album_genres(
106+
db_album.artists[0].name, db_album.name
107+
)
108+
update_album(db_album)
109+
110+
return make_response("Playlist details populated", 201)
111+
112+
@database_controller.route("populate_universal_genre_list", methods=["GET"])
113+
def populate_universal_genre_list():
114+
genre_list = musicbrainz.get_genre_list()
115+
[create_genre(genre) for genre in genre_list]
116+
return make_response("Genre data populated", 201)
117+
118+
@database_controller.route("populate_user_album_genres", methods=["GET"])
119+
def populate_user_album_genres():
120+
access_token = request.cookies.get("spotify_access_token")
121+
user = spotify.get_current_user(access_token)
122+
populate_album_genres_by_user_id(user.id, musicbrainz)
123+
return make_response("User album genres populated", 201)
124+
125+
return database_controller
126+
127+
128+
def split_list(input_list, max_length=20):
129+
return [
130+
input_list[i : i + max_length] for i in range(0, len(input_list), max_length)
131+
]
132+
133+
134+
def populate_album_genres_by_user_id(
135+
user_id: str, musicbrainz: MusicbrainzClient = MusicbrainzClient()
136+
):
137+
albums = get_user_albums(user_id=user_id)
138+
print(f"processing album {0} of {len(albums)}")
139+
skip_count = 0
140+
for idx, db_album in enumerate(albums):
141+
print("\033[A \033[A")
142+
print(f"processing album {idx} of {len(albums)}, skipped {skip_count}")
143+
if get_album_genres(db_album.id) != []:
144+
skip_count += 1
145+
continue
146+
album_artists = get_album_artists(db_album)
147+
genres = musicbrainz.get_album_genres(
148+
artist_name=album_artists[0].name, album_title=db_album.name
149+
)
150+
add_genres_to_album(db_album, genres)
151+
print("\033[A \033[A")
152+
print(f"completed. Processed {len(albums)} albums. Skipped ")

backend/src/controllers/spotify.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
from logging import Logger
12
from flask import Blueprint, make_response, request
23
from src.dataclasses.playback_info import PlaybackInfo
34
from src.dataclasses.playback_request import StartPlaybackRequest
45
from src.dataclasses.playlist import Playlist
56
from src.spotify import SpotifyClient
7+
import sys
68

79

810
def spotify_controller(spotify: SpotifyClient):

0 commit comments

Comments
 (0)