Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
12fe953
chore: create .gitignore
TimNekk Dec 4, 2023
94e38ba
feat: add .idea to gitignore
TimNekk Dec 5, 2023
5b7d780
req: add spotipy and environs
TimNekk Dec 5, 2023
3ba99c0
feat: add config
TimNekk Dec 5, 2023
bce897a
feat: add device model
TimNekk Dec 5, 2023
5f1e996
feat: add track model
TimNekk Dec 5, 2023
90ec0bc
feat: add spotify class
TimNekk Dec 5, 2023
79fa7fe
feat: add exceptions
TimNekk Dec 5, 2023
c03ae68
feat: add docker support
TimNekk Dec 6, 2023
1f436b3
chore: remove old files
TimNekk Dec 6, 2023
4f5484e
chore: add uvicorn settings
TimNekk Dec 6, 2023
a7f87ab
req: add fastapi
TimNekk Dec 6, 2023
0c3a24c
feat: add spotify router
TimNekk Dec 6, 2023
271dafa
feat: add spotify schemas
TimNekk Dec 6, 2023
884f7a5
feat: add spotify service
TimNekk Dec 6, 2023
06edb93
feat: add system router
TimNekk Dec 6, 2023
d033312
feat: add main app
TimNekk Dec 6, 2023
4dd70be
req: add dependency injector
TimNekk Dec 6, 2023
325a35f
chore: add env vars for docker compose
TimNekk Dec 6, 2023
67e1cb5
chore: move some settings to config.ini
TimNekk Dec 6, 2023
f876382
feat: add spotify service
TimNekk Dec 6, 2023
42dcb2b
fix: add property instead of post init
TimNekk Dec 6, 2023
a555b3a
fix: remove cbv
TimNekk Dec 6, 2023
cc9ebc8
chore: remove config
TimNekk Dec 6, 2023
9a1b4f8
feat: add spotify exceptions
TimNekk Dec 6, 2023
353d80d
feat: add container for injection
TimNekk Dec 6, 2023
6b5d8e0
feat: inject spotify service to router
TimNekk Dec 6, 2023
40359af
feat: initialize container
TimNekk Dec 6, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CONTAINER_NAME=music_box_api
IMAGE_NAME=music_box
EXPOSED_PORT=8000

SPOTIFY_CLIENT_ID=spotifyclientid
SPOTIFY_CLIENT_SECRET=spotifyclientsecret
160 changes: 160 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# 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/#use-with-ide
.pdm.toml

# 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/
8 changes: 8 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM python:3.10-slim

WORKDIR /code

COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

COPY ./app /code/app
20 changes: 20 additions & 0 deletions app/container.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from dependency_injector import containers, providers

from app.spotify.service import SpotifyService


class Container(containers.DeclarativeContainer):
wiring_config = containers.WiringConfiguration(
packages=[
"app",
]
)

config = providers.Configuration(ini_files=["config.ini"], strict=True)

spotify_service = providers.Singleton(
SpotifyService,
client_id=config.spotify.client_id,
client_secret=config.spotify.client_secret,
redirect_uri=config.spotify.redirect_uri
)
33 changes: 33 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from fastapi import FastAPI
from pydantic import TypeAdapter

from app.container import Container
from app.spotify.router import spotify_router
from app.system.router import system_router

container = Container()


def create_application() -> FastAPI:
application = FastAPI(
title="MusicBox API",
debug=True
)

application.include_router(system_router)
application.include_router(spotify_router)

return application


if __name__ == "__main__":
import uvicorn

uvicorn.run(
"app.main:create_application",
factory=True,
host=container.config.uvicorn.host(),
port=int(container.config.uvicorn.port()),
log_level=container.config.uvicorn.log_level(),
reload=TypeAdapter(bool).validate_python(container.config.uvicorn.reload()),
)
Empty file added app/spotify/__init__.py
Empty file.
25 changes: 25 additions & 0 deletions app/spotify/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class SpotifyError(Exception):
"""Base class for exceptions in this module."""
pass


class TrackNotFoundError(SpotifyError):
"""Exception raised when a track is not found.

Attributes:
message -- explanation of the error
"""

def __init__(self, message: str):
self.message = message


class DeviceNotFoundError(SpotifyError):
"""Exception raised when a device is not found.

Attributes:
message -- explanation of the error
"""

def __init__(self, message: str):
self.message = message
16 changes: 16 additions & 0 deletions app/spotify/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from dependency_injector.wiring import inject, Provide
from fastapi import APIRouter, Depends

from app.container import Container
from app.spotify.schemas import Device
from app.spotify.service import SpotifyService

spotify_router = APIRouter()


@spotify_router.get("/devices")
@inject
def get_devices(
spotify_service: SpotifyService = Depends(Provide[Container.spotify_service]),
) -> list[Device]:
return spotify_service.refresh_devices()
20 changes: 20 additions & 0 deletions app/spotify/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from pydantic import BaseModel


class Device(BaseModel):
id: str
is_active: bool
is_private_session: bool
is_restricted: bool
name: str
supports_volume: bool
type: str
volume_percent: int


class Track(BaseModel):
id: str

@property
def uri(self):
return f"spotify:track:{self.id}"
77 changes: 77 additions & 0 deletions app/spotify/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import spotipy

from app.spotify.exceptions import DeviceNotFoundError, TrackNotFoundError
from app.spotify.schemas import Device, Track


class SpotifyService:
def __init__(
self,
client_id: str,
client_secret: str,
redirect_uri: str
) -> None:
self.__api = spotipy.Spotify(
auth_manager=spotipy.SpotifyOAuth(
client_id=client_id,
client_secret=client_secret,
redirect_uri=redirect_uri,
scope="user-read-playback-state,user-modify-playback-state"
)
)

self.__devices = self.__get_devices_from_api()
self.__current_device_index = 0

def play(self, track: Track) -> None:
current_device = self.get_current_device()
if current_device is None:
raise DeviceNotFoundError("No device found")

try:
self.__api.transfer_playback(
device_id=current_device.id,
force_play=False
)
self.__api.start_playback(
device_id=current_device.id,
uris=[track.uri]
)
except spotipy.exceptions.SpotifyException as e:
if "Device not found" in e.msg:
raise DeviceNotFoundError(e.msg)
elif "Invalid track uri" in e.msg:
raise TrackNotFoundError(e.msg)

def refresh_devices(self) -> list[Device]:
self.__devices = self.__get_devices_from_api()
self.__current_device_index = 0
return self.__devices

def next_device(self) -> Device | None:
if len(self.__devices) == 0:
return None

self.__current_device_index = (self.__current_device_index + 1) % len(self.__devices)
return self.get_current_device()

def previous_device(self) -> Device | None:
if len(self.__devices) == 0:
return None

self.__current_device_index = (self.__current_device_index - 1) % len(self.__devices)
return self.get_current_device()

def get_current_device(self) -> Device | None:
if len(self.__devices) == 0:
return None

return self.__devices[self.__current_device_index]

def get_devices(self) -> list[Device]:
print(self.__devices)
return self.__devices

def __get_devices_from_api(self) -> list[Device]:
devices_json = self.__api.devices()["devices"]
return [Device(**device) for device in devices_json]
Empty file added app/system/__init__.py
Empty file.
15 changes: 15 additions & 0 deletions app/system/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from fastapi import APIRouter, status
from starlette.responses import JSONResponse

system_router = APIRouter()


@system_router.get("/", include_in_schema=False)
async def root(self) -> dict[str, str]:
return {"message": "MusicBox API"}


@system_router.get("/health", include_in_schema=False)
async def health(self) -> JSONResponse:
data = {"music-box-api": status.HTTP_200_OK}
return JSONResponse(data, status_code=status.HTTP_200_OK)
Loading