From 17f51e757a7d8182fc388c61de19437207761990 Mon Sep 17 00:00:00 2001 From: mathayay Date: Sat, 6 Dec 2025 13:21:03 +0100 Subject: [PATCH] POC --- final-loop/Dockerfile | 15 ++++- final-loop/compose.yml | 10 ++- final-loop/requirements.txt | 11 ++++ final-loop/src/.cache | 1 + final-loop/src/decode_barcode.py | 27 ++++---- final-loop/src/get_heights.py | 8 +-- final-loop/src/get_uri.py | 101 ++++++++++++++++++++++++++--- final-loop/src/img_cache/frame.jpg | Bin 0 -> 5088 bytes final-loop/src/songs.json | 2 + final-loop/src/start_song.py | 7 +- 10 files changed, 147 insertions(+), 35 deletions(-) create mode 100644 final-loop/src/.cache create mode 100644 final-loop/src/img_cache/frame.jpg create mode 100644 final-loop/src/songs.json 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 0000000000000000000000000000000000000000..7327bbb6ac9a7d05f8dd296a1f4430f360acfc1c GIT binary patch literal 5088 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 YmZP-2*>|mE*BIvrRQ_{sK|}}s2jNY&1poj5 literal 0 HcmV?d00001 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()