Skip to content
This repository was archived by the owner on Feb 28, 2025. It is now read-only.

Commit 296caeb

Browse files
author
Marlon (esolitos) Saglia
committed
fix: ensure REDIS is purely optional
1 parent 196b39b commit 296caeb

File tree

5 files changed

+79
-65
lines changed

5 files changed

+79
-65
lines changed

README.md

+4-8
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ or you can do all via a browser. The first option still requires a browser to
1010
run the OAuth authentication process 'tho.
1111

1212
In either of the two configurations you'll need [Pipenv](https://pipenv.pypa.io/en/latest/)
13-
to install the required libraries.
13+
to install the required libraries.
1414
provide the OAuth client id and
1515
secret as required by [Spotipy](https://spotipy.readthedocs.io/), those should
1616
be passed as environment variables:
@@ -20,27 +20,23 @@ export SPOTIPY_CLIENT_ID='your-spotify-client-id'
2020
export SPOTIPY_CLIENT_SECRET='your-spotify-client-secret'
2121
```
2222

23-
24-
2523
### Running without http server (only CLI)
2624

2725
The script `spotify_weekly.py` can be run via
2826

2927
```shell script
30-
export SPOTIPY_REDIRECT_URI='http://localhost/'
28+
export REDIRECT_HOST='localhost:80'
3129
```
3230

33-
3431
## Configuration overview
3532

36-
_All configuration is done via environment variables_
33+
*All configuration is done via environment variables*
3734

3835
| Name | Required | Description |
3936
|---------------------------|-----------|-------------|
4037
| `SPOTIPY_CLIENT_ID` | Yes | Oauth Client ID, by Spotify |
4138
| `SPOTIPY_CLIENT_SECRET` | Yes | Oauth Client Secret, by Spotify |
42-
| `SPOTIPY_REDIRECT_URI` | CLI Only | Only when run via CLI. Read "Running without http server" for more info |
4339
| `OAUTH_CACHE_FILE` | No | *(Only HTTP)* Sets the oauth cache file path. Must be writable. |
4440
| `SERVER_HOST` | No | *(Only HTTP)* Http server IP (Default: `127.0.1.1`) |
4541
| `SERVER_PORT` | No | *(Only HTTP)* Http server Port (Default: `8080` |
46-
| `REDIRECT_HOST` | No | *(Only HTTP)* Redirect hostname for Spotify oauth. (Default: "`$SERVER_HOST:$SERVER_PORT`") |
42+
| `REDIRECT_HOST` | No | *(Only HTTP)* Redirect hostname for Spotify oauth. (Default: "`$SERVER_HOST:$SERVER_PORT`") |

swa/session.py

+51-46
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,39 @@
1+
12
"""
23
A module to manage user sessions.
34
4-
This module provides classes and functions to manage user sessions, storing the data in Redis.
5+
This module provides classes and functions to manage user sessions. It now supports storing the data in Redis,
6+
and falls back to file-based storage when REDIS_URL is not provided.
57
"""
68

79
from __future__ import annotations
810
import json
911
import logging
1012
import random
1113
import string
12-
13-
from os import getenv
14+
import os
15+
from typing import Optional
1416

1517
import bottle
1618
from swa.spotifyoauthredis import access_token
1719
from swa.utils import redis_client, redis_session_data_key
1820

19-
COOKIE_SECRET = str(getenv("SPOTIPY_CLIENT_SECRET", "default"))
21+
COOKIE_SECRET = str(os.getenv("SPOTIPY_CLIENT_SECRET", "default"))
22+
23+
# File-based storage directory
24+
FILE_STORAGE_PATH = './session_data'
25+
26+
27+
def is_redis_enabled() -> bool:
28+
"""
29+
Check if Redis is enabled by looking for REDIS_URL.
30+
Returns True if REDIS_URL is set, False otherwise.
31+
"""
32+
return 'REDIS_URL' in os.environ
33+
34+
35+
def get_file_storage_path(session_id: str) -> str:
36+
return os.path.join(FILE_STORAGE_PATH, f'{session_id}.json')
2037

2138

2239
class SessionData:
@@ -76,16 +93,15 @@ def to_json(self) -> str:
7693

7794

7895
def session_start() -> str:
79-
"""
80-
Starts a session, setting the data in Redis.
96+
session_id = ''.join(random.choices(
97+
string.ascii_letters + string.digits, k=16))
98+
bottle.response.set_cookie('SID', session_id, secret=COOKIE_SECRET)
8199

82-
:return: The session ID for the new session.
83-
"""
84-
session_id = ''.join(random.choices(string.ascii_letters + string.digits, k=16))
85-
bottle.response.set_cookie('SID', session_id, secret=COOKIE_SECRET, path='/', httponly=True)
86-
redis_key = redis_session_data_key(session_id)
87-
redis_data = SessionData({'id': session_id}).to_json()
88-
redis_client().set(name=redis_key, value=redis_data)
100+
# Initialize empty session data
101+
if not is_redis_enabled():
102+
os.makedirs(FILE_STORAGE_PATH, exist_ok=True)
103+
with open(get_file_storage_path(session_id), 'w') as file:
104+
json.dump({}, file)
89105

90106
return session_id
91107

@@ -97,62 +113,51 @@ def session_get_id(auto_start: bool = True) -> str | None:
97113
:param auto_start: If True (the default), a new session will be started if necesary.
98114
:return: The session ID or None if no session is active and `auto_start` is False.
99115
"""
100-
session_id = bottle.request.get_cookie('SID')
116+
session_id = bottle.request.get_cookie('SID', secret=COOKIE_SECRET)
101117
if session_id is None:
102118
return session_start() if auto_start else None
103119

104120
return str(session_id)
105121

106122

107-
def session_get_data(session_id=None) -> SessionData:
108-
"""
109-
Returns the SessionData instance for the current session or the given session ID.
110-
111-
:param session_id: The session ID to get the data for.
112-
If not provided, the current session ID will be used.
113-
114-
:return: The `SessionData` instance for the current or given session.
115-
:raises RuntimeError: If no session is active and no `session_id` is provided.
116-
"""
123+
def session_get_data(session_id: str = None) -> SessionData:
117124
if not session_id:
118125
session_id = session_get_id(auto_start=False)
119126

120127
if not session_id:
121128
raise RuntimeError('No valid session and no session_id provided!')
122129

123-
redis_data = redis_client().get(redis_session_data_key(session_id))
124-
logging.debug("Session data: ", end=" ")
125-
logging.debug({session_id: redis_data})
126-
if redis_data:
127-
return SessionData.from_json(redis_data)
130+
if is_redis_enabled():
131+
redis_data = redis_client().get(redis_session_data_key(session_id))
132+
if redis_data:
133+
return SessionData.from_json(redis_data)
134+
else:
135+
try:
136+
with open(get_file_storage_path(session_id), 'r') as file:
137+
return SessionData(json.load(file))
138+
except FileNotFoundError:
139+
return SessionData()
128140

129141
return SessionData()
130142

131-
def session_set_data(data: SessionData, session_id: str = None) -> bool:
132-
"""
133-
Sets the session data for the current or given session.
134-
135-
:param data: The `SessionData` instance to set for the session.
136-
:param session_id: The session ID to set the data for.
137-
If not provided, the current session ID will be used.
138143

139-
:return: True if the data was successfully set, or False otherwise.
140-
:raises RuntimeError: If no session is active and no `session_id` is provided.
141-
"""
144+
def session_set_data(data: SessionData, session_id: str = None) -> bool:
142145
if not session_id:
143146
session_id = session_get_id(False)
144147

145148
if not session_id:
146149
raise RuntimeError('No valid session and no session_id provided!')
147150

148-
redis_key = redis_session_data_key(session_id)
149-
redis_data = data.to_json()
150-
logging.debug("Set session data: ", end=" ")
151-
logging.debug({session_id: redis_data})
152-
if not redis_client().set(name=redis_key, value=redis_data):
153-
return False
151+
if is_redis_enabled():
152+
redis_key = redis_session_data_key(session_id)
153+
redis_data = data.to_json()
154+
return redis_client().set(name=redis_key, value=redis_data)
155+
else:
156+
os.makedirs(FILE_STORAGE_PATH, exist_ok=True)
157+
with open(get_file_storage_path(session_id), 'w') as file:
158+
json.dump(data.all(), file)
159+
return True
154160

155-
return True
156161

157162
def session_get_oauth_token() -> tuple(SessionData, str):
158163
"""

swa/spotify_weekly.py

+20-9
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,27 @@
1111
from typing import Dict, List, Optional
1212
from spotipy import Spotify
1313

14+
1415
class SwaError(RuntimeError):
1516
"""Generic App error."""
1617

18+
1719
class PlaylistError(SwaError):
1820
"""Playlist generic error."""
1921

22+
2023
class DiscoverWeeklyError(PlaylistError):
2124
"""Generic error about 'Discover Weekly' playlist."""
2225

26+
2327
class DiscoverWeeklyNotFoundError(DiscoverWeeklyError):
2428
"""Playlist 'Discover Weekly' not found."""
2529

30+
2631
class DiscoverWeeklyMultipleMatchesError(DiscoverWeeklyError):
2732
"""Playlist 'Discover Weekly' has multiple matches."""
2833

34+
2935
class SwaRunner:
3036
"""A class to run a script that fetches tracks from the user's "Discover Weekly" playlist,
3137
and adds them to a new playlist with only albums.
@@ -86,7 +92,8 @@ def get_user_playlists(self, sort_by_author: bool = False) -> List[Dict]:
8692
"""
8793
key: str = 'user_playlists'
8894
if key not in self._cache or self._cache[key] is None:
89-
self._cache[key] = self._spy_client.current_user_playlists()['items']
95+
self._cache[key] = self._spy_client.current_user_playlists()[
96+
'items']
9097

9198
if sort_by_author:
9299
return self._sort_playlists_by_author(self._cache[key])
@@ -118,7 +125,8 @@ def get_discover_weekly(self, allow_multiple: bool = False) -> list or dict:
118125

119126
playlist_name: str = 'Discover Weekly'
120127
if playlist_name not in self._cache or self._cache[playlist_name] is None:
121-
self._cache[playlist_name] = self.get_playlist_by_name(playlist_name, multiple=True)
128+
self._cache[playlist_name] = self.get_playlist_by_name(
129+
playlist_name, multiple=True)
122130

123131
if len(self._cache[playlist_name]) <= 0:
124132
raise DiscoverWeeklyNotFoundError()
@@ -132,9 +140,11 @@ def prepare_weekly_album_playlist(self) -> dict:
132140
"""
133141
Attempts to find the "Weekly Album discovery", cleaning it up is needed.
134142
"""
135-
album_playlist = self.get_playlist_by_name(self._special_playlist['name'])
143+
album_playlist = self.get_playlist_by_name(
144+
self._special_playlist['name'])
136145
if not album_playlist:
137-
logging.debug("Creating playlist: '%s'", self._special_playlist['name'])
146+
logging.debug("Creating playlist: '%s'",
147+
self._special_playlist['name'])
138148
return self._spy_client.user_playlist_create(
139149
self.get_username(),
140150
name=self._special_playlist['name'],
@@ -144,16 +154,16 @@ def prepare_weekly_album_playlist(self) -> dict:
144154

145155
logging.info("Found playlist '%s:'", self._special_playlist['name'])
146156
if album_playlist['tracks']['total'] > 0:
147-
logging.info("Contains %s tracks to remove.", album_playlist['tracks']['total'])
157+
logging.info("Contains %s tracks to remove.",
158+
album_playlist['tracks']['total'])
148159
self._playlist_cleanup(album_playlist['id'])
149160

150161
return album_playlist
151162

152163
def _playlist_cleanup(self, playlist_id: str):
153-
logging.info('Cleaning up:', end=' ')
164+
logging.info('Cleaning up playlist: %s', playlist_id)
154165
while self._do_playlist_cleanup(playlist_id):
155-
logging.info('.', end='')
156-
logging.info('!')
166+
pass
157167

158168
def _do_playlist_cleanup(self, playlist_id: str):
159169
playlist_tracks = self._spy_client.playlist_tracks(
@@ -188,7 +198,8 @@ def get_all_albums_tracks(self, album_ids: list):
188198
"""
189199
tracks = []
190200
for album_id in album_ids:
191-
tracks.extend([t['id'] for t in self._spy_client.album_tracks(album_id)['items']])
201+
tracks.extend(
202+
[t['id'] for t in self._spy_client.album_tracks(album_id)['items']])
192203

193204
return tracks
194205

swa/spotifyoauthredis.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def spotify_oauth(email: str) -> spotipy.SpotifyOAuth:
3535
redirect_url = f'http://{hostname}/oauth/callback'
3636

3737
cache_handler = None
38-
if getenv('REDIS_URL') is not None:
38+
if getenv('REDIS_URL'):
3939
rclient = redis.Redis().from_url(url=getenv('REDIS_URL'), decode_responses=True)
4040
cache_handler = spotipy.cache_handler.RedisCacheHandler(
4141
rclient,

swa_http.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def login():
4747
def do_login():
4848
"""Handles user login and redirects to Spotify authorization page."""
4949
session_id = sws.session_get_id(auto_start=True)
50+
logging.debug(session_id)
5051
email = str(bottle.request.forms.get('email'))
5152
session_data = sws.session_get_data(session_id).add('email', email)
5253
logging.debug(session_data)
@@ -238,7 +239,6 @@ def check_requirements():
238239
required_vars = [
239240
"SPOTIPY_CLIENT_ID",
240241
"SPOTIPY_CLIENT_SECRET",
241-
"SPOTIPY_REDIRECT_URI",
242242
]
243243
for var in required_vars:
244244
if var not in os.environ:
@@ -254,6 +254,8 @@ def main():
254254
server_host, server_port = swutil.http_server_info()
255255
enable_debug = bool(os.getenv('DEBUG'))
256256
check_requirements()
257+
if enable_debug:
258+
logging.basicConfig(level=logging.DEBUG)
257259
bottle.run(
258260
host=server_host, port=server_port,
259261
debug=enable_debug, reloader=(not swutil.is_prod())

0 commit comments

Comments
 (0)