diff --git a/final-loop/Dockerfile b/final-loop/Dockerfile index 71c5ea5..7be1e9e 100644 --- a/final-loop/Dockerfile +++ b/final-loop/Dockerfile @@ -1,10 +1,19 @@ FROM python:3.11-slim +ENV DEBIAN_FRONTEND=noninteractive + WORKDIR /app -COPY requirements.txt ./ +# Install only packages that exist in Debian Trixie +RUN apt-get update && apt-get install -y \ + libgl1 \ + libglib2.0-0 \ + libsm6 \ + libxext6 \ + libxrender1 \ + && rm -rf /var/lib/apt/lists/* +COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt -COPY src/ . - +COPY src/ . \ No newline at end of file diff --git a/final-loop/compose.yml b/final-loop/compose.yml index 51b0d73..29162d1 100644 --- a/final-loop/compose.yml +++ b/final-loop/compose.yml @@ -22,13 +22,19 @@ services: build: . container_name: spotvinyl devices: - - /dev/video2 + - /dev/video0:/dev/video0 + - /dev/video1:/dev/video1 + - /dev/video2:/dev/video2 + - /dev/video3:/dev/video3 + volumes: - ./src:/app # - ./spotvinyl-cache working_dir: /app + environment: + - PYTHONUNBUFFERED=1 env_file: - .env - command: python3 decode_barcode.py + command: python3 -u decode_barcode.py ports: - "5000:5000" diff --git a/final-loop/requirements.txt b/final-loop/requirements.txt index f58bc59..67813e2 100644 --- a/final-loop/requirements.txt +++ b/final-loop/requirements.txt @@ -1,22 +1,31 @@ +blinker==1.9.0 certifi==2025.11.12 chardet==5.2.0 charset-normalizer==3.4.4 +click==8.3.1 contourpy==1.3.3 crccheck==1.3.1 cycler==0.12.1 decorator==5.2.1 +Flask==3.1.2 fonttools==4.60.1 +greenlet==3.2.4 idna==3.11 ImageIO==2.37.2 +itsdangerous==2.2.0 +Jinja2==3.1.6 joblib==1.5.2 kiwisolver==1.4.9 lazy_loader==0.4 +MarkupSafe==3.0.3 matplotlib==3.10.7 networkx==3.5 numpy==2.2.6 opencv-python==4.12.0.88 packaging==25.0 pillow==12.0.0 +playwright==1.56.0 +pyee==13.0.0 pyparsing==3.2.5 python-dateutil==2.9.0.post0 PyWavelets==1.9.0 @@ -29,4 +38,6 @@ six==1.17.0 spotipy==2.25.1 threadpoolctl==3.6.0 tifffile==2025.10.16 +typing_extensions==4.15.0 urllib3==2.5.0 +Werkzeug==3.1.3 diff --git a/final-loop/src/.cache b/final-loop/src/.cache new file mode 100644 index 0000000..8a19bf3 --- /dev/null +++ b/final-loop/src/.cache @@ -0,0 +1 @@ +{"access_token": "BQDrlnB0kiEzwgXmPTK1_c4IfsmaAxTBhIy6XdV8WLETUahlOzbqZjsCnt75YR-IyPUplWlHpcxStmm_yw3IyGhu6cLIuhX1A1tfndQhNERescv7pkW9vFOx2VNhSq_u4z66_C0cKeC1zxGrB5mP03YQfOgC1jTdT6qzfThHd1_XJ_c5LogWiWNH8B8lxaKEmVi-sVJKldynGTuj6nU3HA6G8jYef1YGK_34f1qwsLYOcYUN0tjfUA", "token_type": "Bearer", "expires_in": 3600, "scope": "user-modify-playback-state user-read-playback-state", "expires_at": 1765023017, "refresh_token": "AQAUKbWNL0mkEiOaoWfodsTnE2r8pqocU7XCRq6MvSLXlJGU5Wc6QaTRgL24EIVVOvjBzucYbMQ571v-GzbTCK02EDQfLBgWcslm9RnwtfVScGCYvl1mpzSJF0cK-I9OIUo"} \ No newline at end of file diff --git a/final-loop/src/decode_barcode.py b/final-loop/src/decode_barcode.py index a56cfdd..a601fab 100644 --- a/final-loop/src/decode_barcode.py +++ b/final-loop/src/decode_barcode.py @@ -11,24 +11,27 @@ import threading import time import numpy as np -os.environ["QT_QPA_PLATFORM"] = "xcb" -def process_frame(frame, token): +os.environ["QT_QPA_PLATFORM"] = "offscreen" +os.makedirs("/app/img_cache", exist_ok=True) # create folder if missing +def process_frame(frame): heights,preprocess = get_heights(frame) - cv2.imshow("frame", preprocess) + cv2.imwrite("/app/img_cache/frame.jpg", preprocess) if len(heights)!=23: return None # skip bad frames else: print("ON TROUVE UN CODE") - + if heights == [0, 4, 2, 6, 5, 7, 2, 3, 1, 5, 5, 7, 4, 6, 4, 1, 2, 0, 6, 7, 3, 5, 0]: + print("VICTOIREEEEEEE") heights = heights[1:11] + heights[12:-1] decoded = spotify_bar_decode(heights) - uri = get_uri(decoded, token) - summary, full_response = get_info(uri["target"], token) - print(f"SONG FOUND : {summary["name"]}") + if decoded == -1: + return None + uri = get_uri(decoded) + print(uri) if uri: - start_song(uri['target'],os.getenv('DEVICE_ID')) + start_song(uri,os.getenv('DEVICE_ID')) return summary @@ -44,13 +47,9 @@ if __name__ == "__main__": - cap = cv2.VideoCapture('/dev/video0') + cap = cv2.VideoCapture('/dev/video2') - token = os.getenv('ACCESS_TOKEN') - if not token: - token = "BQCph85n2Cr-h8jKWhsdsdhywaX3h_bn4pJ_-jdque2u_gn9bI-OdthIowGSU6r058QozL0eJfzy_ClWezXYKrQO2npuyfWVphxSQrKqhBWkGr5bK0UrIfsKKAdJvoNrXD9Db-ObgP5D3-rMpF0Xq3RXwMpTal9NpzTJcHZs_PBjbNClJVy24Jk5WfGbKZPkMs_Hon5TjABx4QzxzE2vxjd4X4EyPlyPuKiIVp-f7yTSJbbRLqt-_O_VJ9mnQ1RgGK16afY7p3JZH_B6-VSCrFuhK_m9yhSieiWoqEeopFEX47Nc4-tuqe8CXcYGiRLZBIcc4w64ly36ZIftxRN7ehcJb2gcV26ZqMS1lg1Yxp0OD4ShJJsinA69X535_w" - print(token) if not cap.isOpened(): @@ -71,7 +70,7 @@ if __name__ == "__main__": try: - summary = process_frame(frame, token) + summary = process_frame(frame) except Exception as e: print(e) diff --git a/final-loop/src/get_heights.py b/final-loop/src/get_heights.py index a2a6e50..21cc8ed 100644 --- a/final-loop/src/get_heights.py +++ b/final-loop/src/get_heights.py @@ -27,12 +27,12 @@ def get_heights(image): im = rgb2gray(image) - + im_eq = exposure.equalize_adapthist(im, clip_limit=0.01) # Filtrage gaussien léger pour diminuer bruit - im_blur = gaussian_filter(im, sigma=1) + im_blur = gaussian_filter(im_eq, sigma=1) # Calcul du seuil local avec block_size impair, ajuster offset si besoin - th = threshold_local(im_blur, block_size=71, offset=-0.09) + th = threshold_local(im_blur, block_size=31, offset=-0.14) binary_im = im_blur > th @@ -42,7 +42,7 @@ def get_heights(image): bar_dims = [r.bbox for r in regionprops(labeled)] bar_dims.sort(key=lambda x: x[1]) # left to right - bars = bar_dims#[1:] # skip logo + bars = bar_dims[1:] # skip logo bar_heights_raw = [] if(len(bars)!=23): print(len(bars)) diff --git a/final-loop/src/get_uri.py b/final-loop/src/get_uri.py index 5937308..21ca23c 100644 --- a/final-loop/src/get_uri.py +++ b/final-loop/src/get_uri.py @@ -1,5 +1,73 @@ from typing import Tuple import requests +import spotipy +from spotipy.oauth2 import SpotifyOAuth +import sys +from urllib.parse import urlencode +from urllib.parse import urlencode, urlparse, parse_qs +import webbrowser +import time +import base64 + + +CLIENT_ID = 'a1b29f64bef643b5ade0944830637510' +CLIENT_SECRET = '1d74196e6fec41f9986917afd57df3da' +REDIRECT_URI = 'https://vinyly.couraud.xyz' +SCOPE = 'user-modify-playback-state user-read-playback-state' +TOKEN_FILE = 'spotify_token.json' +import json + +def get_spotify_token(): + # Try to load existing token + try: + with open(TOKEN_FILE) as f: + data = json.load(f) + if data['expires_at'] > time.time(): + return data['access_token'] + # refresh token + resp = requests.post( + 'https://accounts.spotify.com/api/token', + data={'grant_type': 'refresh_token', 'refresh_token': data['refresh_token']}, + headers={'Authorization': 'Basic ' + base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()} + ).json() + data['access_token'] = resp['access_token'] + data['expires_at'] = time.time() + resp['expires_in'] + with open(TOKEN_FILE, 'w') as f2: + json.dump(data, f2) + return data['access_token'] + except FileNotFoundError: + # Get authorization code + auth_url = 'https://accounts.spotify.com/authorize?' + urlencode({ + 'client_id': CLIENT_ID, + 'response_type': 'code', + 'redirect_uri': REDIRECT_URI, + 'scope': SCOPE + }) + print("Open this URL and authorize:", auth_url) + webbrowser.open(auth_url) + code = input("Paste the full redirected URL here: ") + code = parse_qs(urlparse(code).query)['code'][0] + + # Exchange code for tokens + resp = requests.post( + 'https://accounts.spotify.com/api/token', + data={ + 'grant_type': 'authorization_code', + 'code': code, + 'redirect_uri': REDIRECT_URI + }, + headers={'Authorization': 'Basic ' + base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()} + ).json() + + data = { + 'access_token': resp['access_token'], + 'refresh_token': resp['refresh_token'], + 'expires_at': time.time() + resp['expires_in'] + } + with open(TOKEN_FILE, 'w') as f: + json.dump(data, f) + return data['access_token'] + HEADERS_LUT = { "X-Client-Id": "58bd3c95768941ea9eb4350aaa033eb3", @@ -11,18 +79,30 @@ HEADERS_LUT = { "Accept-Language": "en", "Spotify-App-Version": "8.5.68", } + MEDIA_REF_LUT_URL = "https://spclient.wg.spotify.com:443/scannable-id/id" -def get_uri(media_ref: int, token: str): - """Query Spotify internal API to get the URI of the media reference.""" - header = { - **HEADERS_LUT, - "Authorization": f"Bearer {token}" - } - url = f'{MEDIA_REF_LUT_URL}/{media_ref}?format=json' - response = requests.get(url, headers=header) - response.raise_for_status() - return response.json() +auth_url = "https://accounts.spotify.com/authorize?" + urlencode({ + "client_id": CLIENT_ID, + "response_type": "code", + "redirect_uri": REDIRECT_URI, + "scope": SCOPE +}) + + + + +def get_uri(media_ref: int): + + with open("/app/songs.json") as f: + code_to_url = json.load(f) + + uri = code_to_url.get(str(media_ref)) + if uri: + return uri + else: + return -1 + def get_info(uri: str, token: str) -> Tuple[dict, dict]: """Query the Spotify API to get information about a URI.""" @@ -54,3 +134,4 @@ def get_info(uri: str, token: str) -> Tuple[dict, dict]: return result, resp + diff --git a/final-loop/src/img_cache/frame.jpg b/final-loop/src/img_cache/frame.jpg new file mode 100644 index 0000000..7327bbb Binary files /dev/null and b/final-loop/src/img_cache/frame.jpg differ diff --git a/final-loop/src/songs.json b/final-loop/src/songs.json new file mode 100644 index 0000000..ed4ed1a --- /dev/null +++ b/final-loop/src/songs.json @@ -0,0 +1,2 @@ +{"6818913446" : "spotify:track:0RoA7ObU6phWpqhlC9zH4Z"} + diff --git a/final-loop/src/start_song.py b/final-loop/src/start_song.py index ac6e89d..411f65b 100644 --- a/final-loop/src/start_song.py +++ b/final-loop/src/start_song.py @@ -9,7 +9,7 @@ REDIRECT_URI = 'https://vinyly.couraud.xyz' SCOPE = 'user-modify-playback-state user-read-playback-state' -def start_song(URI="spotify:track:0RoA7ObU6phWpqhlC9zH4Z",device_id="a499b8f3684d8098ffb5f9ff11392ebdd61fc6d1"): +def start_song(URI="spotify:track:0RoA7ObU6phWpqhlC9zH4Z",device_id="2f97220989834a8d84e8d93860444601fde25f44"): auth_manager = SpotifyOAuth(client_id=CLIENT_ID, client_secret=CLIENT_SECRET, redirect_uri=REDIRECT_URI, @@ -32,12 +32,15 @@ def start_song(URI="spotify:track:0RoA7ObU6phWpqhlC9zH4Z",device_id="a499b8f3684 print('No active Spotify devices found. Please start playback on a device.') sys.exit(1) + print(devices['devices']) current = sp.current_user_playing_track() + current_uri = "" if current and current.get("item"): current_uri = current["item"]["uri"] if current_uri != URI: + print(URI) sp.start_playback(device_id=device_id, uris=[URI]) if __name__ == '__main__': - start_song(URI,device_id) + start_song()