POC
This commit is contained in:
@@ -1,10 +1,19 @@
|
|||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
WORKDIR /app
|
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
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
COPY src/ .
|
COPY src/ .
|
||||||
|
|
||||||
|
|||||||
@@ -22,13 +22,19 @@ services:
|
|||||||
build: .
|
build: .
|
||||||
container_name: spotvinyl
|
container_name: spotvinyl
|
||||||
devices:
|
devices:
|
||||||
- /dev/video2
|
- /dev/video0:/dev/video0
|
||||||
|
- /dev/video1:/dev/video1
|
||||||
|
- /dev/video2:/dev/video2
|
||||||
|
- /dev/video3:/dev/video3
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./src:/app
|
- ./src:/app
|
||||||
# - ./spotvinyl-cache
|
# - ./spotvinyl-cache
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
|
environment:
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
command: python3 decode_barcode.py
|
command: python3 -u decode_barcode.py
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
|
blinker==1.9.0
|
||||||
certifi==2025.11.12
|
certifi==2025.11.12
|
||||||
chardet==5.2.0
|
chardet==5.2.0
|
||||||
charset-normalizer==3.4.4
|
charset-normalizer==3.4.4
|
||||||
|
click==8.3.1
|
||||||
contourpy==1.3.3
|
contourpy==1.3.3
|
||||||
crccheck==1.3.1
|
crccheck==1.3.1
|
||||||
cycler==0.12.1
|
cycler==0.12.1
|
||||||
decorator==5.2.1
|
decorator==5.2.1
|
||||||
|
Flask==3.1.2
|
||||||
fonttools==4.60.1
|
fonttools==4.60.1
|
||||||
|
greenlet==3.2.4
|
||||||
idna==3.11
|
idna==3.11
|
||||||
ImageIO==2.37.2
|
ImageIO==2.37.2
|
||||||
|
itsdangerous==2.2.0
|
||||||
|
Jinja2==3.1.6
|
||||||
joblib==1.5.2
|
joblib==1.5.2
|
||||||
kiwisolver==1.4.9
|
kiwisolver==1.4.9
|
||||||
lazy_loader==0.4
|
lazy_loader==0.4
|
||||||
|
MarkupSafe==3.0.3
|
||||||
matplotlib==3.10.7
|
matplotlib==3.10.7
|
||||||
networkx==3.5
|
networkx==3.5
|
||||||
numpy==2.2.6
|
numpy==2.2.6
|
||||||
opencv-python==4.12.0.88
|
opencv-python==4.12.0.88
|
||||||
packaging==25.0
|
packaging==25.0
|
||||||
pillow==12.0.0
|
pillow==12.0.0
|
||||||
|
playwright==1.56.0
|
||||||
|
pyee==13.0.0
|
||||||
pyparsing==3.2.5
|
pyparsing==3.2.5
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
PyWavelets==1.9.0
|
PyWavelets==1.9.0
|
||||||
@@ -29,4 +38,6 @@ six==1.17.0
|
|||||||
spotipy==2.25.1
|
spotipy==2.25.1
|
||||||
threadpoolctl==3.6.0
|
threadpoolctl==3.6.0
|
||||||
tifffile==2025.10.16
|
tifffile==2025.10.16
|
||||||
|
typing_extensions==4.15.0
|
||||||
urllib3==2.5.0
|
urllib3==2.5.0
|
||||||
|
Werkzeug==3.1.3
|
||||||
|
|||||||
1
final-loop/src/.cache
Normal file
1
final-loop/src/.cache
Normal file
@@ -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"}
|
||||||
@@ -11,24 +11,27 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
os.environ["QT_QPA_PLATFORM"] = "xcb"
|
os.environ["QT_QPA_PLATFORM"] = "offscreen"
|
||||||
def process_frame(frame, token):
|
os.makedirs("/app/img_cache", exist_ok=True) # create folder if missing
|
||||||
|
def process_frame(frame):
|
||||||
|
|
||||||
heights,preprocess = get_heights(frame)
|
heights,preprocess = get_heights(frame)
|
||||||
cv2.imshow("frame", preprocess)
|
cv2.imwrite("/app/img_cache/frame.jpg", preprocess)
|
||||||
if len(heights)!=23:
|
if len(heights)!=23:
|
||||||
return None # skip bad frames
|
return None # skip bad frames
|
||||||
else:
|
else:
|
||||||
print("ON TROUVE UN CODE")
|
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]
|
heights = heights[1:11] + heights[12:-1]
|
||||||
decoded = spotify_bar_decode(heights)
|
decoded = spotify_bar_decode(heights)
|
||||||
uri = get_uri(decoded, token)
|
if decoded == -1:
|
||||||
summary, full_response = get_info(uri["target"], token)
|
return None
|
||||||
print(f"SONG FOUND : {summary["name"]}")
|
uri = get_uri(decoded)
|
||||||
|
print(uri)
|
||||||
if uri:
|
if uri:
|
||||||
start_song(uri['target'],os.getenv('DEVICE_ID'))
|
start_song(uri,os.getenv('DEVICE_ID'))
|
||||||
return summary
|
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():
|
if not cap.isOpened():
|
||||||
@@ -71,7 +70,7 @@ if __name__ == "__main__":
|
|||||||
try:
|
try:
|
||||||
|
|
||||||
|
|
||||||
summary = process_frame(frame, token)
|
summary = process_frame(frame)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ def get_heights(image):
|
|||||||
|
|
||||||
|
|
||||||
im = rgb2gray(image)
|
im = rgb2gray(image)
|
||||||
|
im_eq = exposure.equalize_adapthist(im, clip_limit=0.01)
|
||||||
# Filtrage gaussien léger pour diminuer bruit
|
# 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
|
# 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
|
binary_im = im_blur > th
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ def get_heights(image):
|
|||||||
bar_dims = [r.bbox for r in regionprops(labeled)]
|
bar_dims = [r.bbox for r in regionprops(labeled)]
|
||||||
bar_dims.sort(key=lambda x: x[1]) # left to right
|
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 = []
|
bar_heights_raw = []
|
||||||
if(len(bars)!=23):
|
if(len(bars)!=23):
|
||||||
print(len(bars))
|
print(len(bars))
|
||||||
|
|||||||
@@ -1,5 +1,73 @@
|
|||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
import requests
|
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 = {
|
HEADERS_LUT = {
|
||||||
"X-Client-Id": "58bd3c95768941ea9eb4350aaa033eb3",
|
"X-Client-Id": "58bd3c95768941ea9eb4350aaa033eb3",
|
||||||
@@ -11,18 +79,30 @@ HEADERS_LUT = {
|
|||||||
"Accept-Language": "en",
|
"Accept-Language": "en",
|
||||||
"Spotify-App-Version": "8.5.68",
|
"Spotify-App-Version": "8.5.68",
|
||||||
}
|
}
|
||||||
|
|
||||||
MEDIA_REF_LUT_URL = "https://spclient.wg.spotify.com:443/scannable-id/id"
|
MEDIA_REF_LUT_URL = "https://spclient.wg.spotify.com:443/scannable-id/id"
|
||||||
|
|
||||||
def get_uri(media_ref: int, token: str):
|
auth_url = "https://accounts.spotify.com/authorize?" + urlencode({
|
||||||
"""Query Spotify internal API to get the URI of the media reference."""
|
"client_id": CLIENT_ID,
|
||||||
header = {
|
"response_type": "code",
|
||||||
**HEADERS_LUT,
|
"redirect_uri": REDIRECT_URI,
|
||||||
"Authorization": f"Bearer {token}"
|
"scope": SCOPE
|
||||||
}
|
})
|
||||||
url = f'{MEDIA_REF_LUT_URL}/{media_ref}?format=json'
|
|
||||||
response = requests.get(url, headers=header)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
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]:
|
def get_info(uri: str, token: str) -> Tuple[dict, dict]:
|
||||||
"""Query the Spotify API to get information about a URI."""
|
"""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
|
return result, resp
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
BIN
final-loop/src/img_cache/frame.jpg
Normal file
BIN
final-loop/src/img_cache/frame.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
2
final-loop/src/songs.json
Normal file
2
final-loop/src/songs.json
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{"6818913446" : "spotify:track:0RoA7ObU6phWpqhlC9zH4Z"}
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ REDIRECT_URI = 'https://vinyly.couraud.xyz'
|
|||||||
SCOPE = 'user-modify-playback-state user-read-playback-state'
|
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,
|
auth_manager = SpotifyOAuth(client_id=CLIENT_ID,
|
||||||
client_secret=CLIENT_SECRET,
|
client_secret=CLIENT_SECRET,
|
||||||
redirect_uri=REDIRECT_URI,
|
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.')
|
print('No active Spotify devices found. Please start playback on a device.')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(devices['devices'])
|
||||||
current = sp.current_user_playing_track()
|
current = sp.current_user_playing_track()
|
||||||
|
current_uri = ""
|
||||||
if current and current.get("item"):
|
if current and current.get("item"):
|
||||||
current_uri = current["item"]["uri"]
|
current_uri = current["item"]["uri"]
|
||||||
|
|
||||||
if current_uri != URI:
|
if current_uri != URI:
|
||||||
|
print(URI)
|
||||||
sp.start_playback(device_id=device_id, uris=[URI])
|
sp.start_playback(device_id=device_id, uris=[URI])
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
start_song(URI,device_id)
|
start_song()
|
||||||
|
|||||||
Reference in New Issue
Block a user