diff --git a/.gitignore b/.gitignore index 7440ced..b2da24d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,132 @@ +# Editors +.vscode/ +.idea/ + +# Vagrant +.vagrant/ + +# Mac/OSX +.DS_Store + +# Windows +Thumbs.db + +# Source for the following rules: https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +dist_chrome/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +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 +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# 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 + +# node +node_modules/ back/.users user/.env diff --git a/README.md b/README.md index 7e408a9..77faf50 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ ![music player](Music-Player.gif) -![diagramme](Diagramme.drawio.png) \ No newline at end of file +![diagramme](Diagramme.drawio.png) + +# To do : +- SHA-256 +- not playing state (js) \ No newline at end of file diff --git a/back/Dockerfile b/back/Dockerfile index 9b0a503..40c00ff 100644 --- a/back/Dockerfile +++ b/back/Dockerfile @@ -10,4 +10,4 @@ EXPOSE 3005 RUN pip install gunicorn -CMD ["gunicorn", "-w", "1", "-b", "0.0.0.0:3005", "--chdir", "/t", "app:app"] \ No newline at end of file +CMD ["gunicorn", "-w", "1", "-b", "0.0.0.0:3005", "--chdir", "/t", "--log-level", "debug", "app:app"] \ No newline at end of file diff --git a/back/app.py b/back/app.py index c9e533c..47db84d 100644 --- a/back/app.py +++ b/back/app.py @@ -1,7 +1,7 @@ from flask import Flask, request, jsonify from flask_cors import CORS import json -#import hashlib +import hashlib app = Flask(__name__) CORS(app, resources={r"/music/*": {"origins": "http://*"}}) @@ -16,12 +16,12 @@ def set_content(): data = request.get_json() user = data.get('user') password = data.get('password') - if data['user'] in users and users[data['user']] == data['password']: - # if user in users and users[user] == hashlib.sha256(password.encode()).hexdigest(): + if user in users and users[user] == hashlib.sha256(password.encode()).hexdigest(): + # cache.clear() cache.update(data) cache.pop('user', None) cache.pop('password', None) - return jsonify({'message': 'Content set successfully.'}) + return jsonify({'message': f'Content set successfully.'}) else: return jsonify({'message': 'Invalid user or password.'}), 401 diff --git a/back/server.yaml b/back/docker-compose.yaml similarity index 100% rename from back/server.yaml rename to back/docker-compose.yaml diff --git a/front/player.js b/front/player.js index 9957904..6869a79 100644 --- a/front/player.js +++ b/front/player.js @@ -4,7 +4,8 @@ function secondsToMinutesAndSeconds(seconds) { return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`; } function fetchDataAndAnimate() { - fetch('http://127.0.0.1:5000/music/get') + // fetch('http://192.168.1.58:3005/music/get') + fetch('http://192.168.1.64:3005/music/get') .then(response => response.json()) .then(data => { const artist = data.artist; @@ -15,9 +16,10 @@ function fetchDataAndAnimate() { const itunes_url = data.itunes_url; const name = data.name; const timestamp = parseFloat(data.timestamp); - const decalage = (Date.now()/1000 - timestamp); - let pPosition = Math.round(parseFloat(data.pPosition) + decalage-3); + const decalage = (Date.now() / 1000 - timestamp); + let pPosition = Math.round(parseFloat(data.pPosition) + decalage - 3); const duration = parseFloat(data.duration); + const status = data.status; const titleSongElement = document.querySelector('.title-song'); // titleSongElement.textContent = name; @@ -40,38 +42,55 @@ function fetchDataAndAnimate() { element.href = itunes_url; }); - const totaltimeElement = document.querySelector('.total-time'); - totaltimeElement.textContent = time; + if (status === 'playing') { + const totaltimeElement = document.querySelector('.total-time'); + totaltimeElement.textContent = time; - const lasttimeElement = document.querySelector('.last-time'); - lasttimeElement.textContent = secondsToMinutesAndSeconds(pPosition); - - const rapport = (pPosition / duration) * 100; - const trackElements = document.querySelectorAll('.track'); - trackElements.forEach(element => { - const style = window.getComputedStyle(element, '::after'); - const currentWidth = parseFloat(style.getPropertyValue('width')); - element.style.setProperty('--new-width', `${rapport.toFixed(2)}%`); - }); - - let intervalId; - - function updateProgress() { const lasttimeElement = document.querySelector('.last-time'); lasttimeElement.textContent = secondsToMinutesAndSeconds(pPosition); + const rapport = (pPosition / duration) * 100; const trackElements = document.querySelectorAll('.track'); trackElements.forEach(element => { + const style = window.getComputedStyle(element, '::after'); + const currentWidth = parseFloat(style.getPropertyValue('width')); element.style.setProperty('--new-width', `${rapport.toFixed(2)}%`); }); - pPosition++; - if (pPosition > duration) { - clearInterval(intervalId); - fetchDataAndAnimate(); - } - } - intervalId = setInterval(updateProgress, 1000); + let intervalId; + + function updateProgress() { + const lasttimeElement = document.querySelector('.last-time'); + lasttimeElement.textContent = secondsToMinutesAndSeconds(pPosition); + const rapport = (pPosition / duration) * 100; + const trackElements = document.querySelectorAll('.track'); + trackElements.forEach(element => { + element.style.setProperty('--new-width', `${rapport.toFixed(2)}%`); + }); + pPosition++; + if (pPosition > duration) { + clearInterval(intervalId); + fetchDataAndAnimate(); + } + } + + intervalId = setInterval(updateProgress, 1000); + } + else { + const totaltimeElement = document.querySelector('.total-time'); + totaltimeElement.textContent = time; + + const lasttimeElement = document.querySelector('.last-time'); + lasttimeElement.textContent = time; + + const rapport = (pPosition / duration) * 100; + const trackElements = document.querySelectorAll('.track'); + trackElements.forEach(element => { + const style = window.getComputedStyle(element, '::after'); + const currentWidth = parseFloat(style.getPropertyValue('width')); + element.style.setProperty('--new-width', `100%`); + }); + } }) .catch(error => { console.error('Error fetching data:', error); diff --git a/user/export.applescript b/user/export.applescript new file mode 100644 index 0000000..4f37103 --- /dev/null +++ b/user/export.applescript @@ -0,0 +1,16 @@ +tell application "Music" + if it is running then + if player state is playing then + set pState to player state + set pPosition to player position + set cTrack to current track + + set trackInfo to "{\"status\": \"playing\", \"persistent ID\": \"" & persistent ID of cTrack & "\", \"name\": \"" & name of cTrack & "\", \"time\": \"" & time of cTrack & "\", \"duration\": \"" & duration of cTrack & "\", \"artist\": \"" & artist of cTrack & "\", \"album artist\": \"" & album artist of cTrack & "\", \"composer\": \"" & composer of cTrack & "\", \"album\": \"" & album of cTrack & "\", \"genre\": \"" & genre of cTrack & "\", \"played count\": \"" & played count of cTrack & "\", \"pState\": \"" & pState & "\", \"pPosition\": \"" & pPosition & "\" }" + return trackInfo + else + return "{\"status\": \"not playing\"}" + end if + else + return "{\"status\": \"not running\"}" + end if +end tell \ No newline at end of file diff --git a/user/export.py b/user/export.py new file mode 100644 index 0000000..5b946e7 --- /dev/null +++ b/user/export.py @@ -0,0 +1,104 @@ +import subprocess +import sys +import time +from datetime import datetime +import json +import requests +from pprint import pprint +import os +from dotenv import load_dotenv +load_dotenv() +USER = os.getenv("USER") +PASSWORD = os.getenv("PASSWORD") + +stdout_file = 'logfile.log' +stderr_file = 'error_logfile.log' + +sys.stdout = open(stdout_file, 'a') +sys.stderr = open(stderr_file, 'a') + +def printout(content): + print(f"{datetime.now().strftime('%H:%M:%S')} : {content}", file=sys.stdout) + sys.stdout.flush() + +def printerr(content): + print(f"{datetime.now().strftime('%H:%M:%S')} : An error occurred: {str(content)}", file=sys.stderr) + sys.stderr.flush() + +def get_current_song(): + try: + output = subprocess.check_output(['osascript', 'export.applescript']).decode('utf-8').strip() + return output + except subprocess.CalledProcessError as e: + printerr(e) + return e + +def get_track_extras(song, artist, album): + query = f"{song} {artist} {album}" + params = {"media": "music", "entity": "song", "term": query} + + r = requests.get("https://itunes.apple.com/search", params=params) + json_data = r.json() + if json_data["resultCount"] == 1: + result = json_data["results"][0] + elif json_data["resultCount"] > 1: + result = json_data["results"][0] + else : + result = '' + + artwork_url = result["artworkUrl100"] if result else None + itunes_url = result["trackViewUrl"] if result else None + artist_url = result["artistViewUrl"] if result else None + + return (artwork_url, itunes_url, artist_url) + +def post(currentsong): + currentsong['user'] = USER + currentsong['password'] = PASSWORD + data = json.dumps(currentsong) + try: + r = requests.post(url+'/music/set', data=data, headers=headers) + if r.status_code != 200: + return r.status_code + else : + return r.text + except Exception as e: + print(f"An error occurred: {str(e)}", file=sys.stderr) + +url = "https://api.noh.am" #RUN WEB +url = "http://127.0.0.1:5000" #DEV +url = "http://0.0.0.0:3005" + +headers = {'Content-Type': 'application/json'} + +def main(): + persistendId = '' + prevstatus = '' + while True: + # print('getting data..', file=sys.stdout) + # currentsong = json.loads(str(get_current_song()).replace("'''", '"')) + currentsong = json.loads(get_current_song()) + + if currentsong['status'] == 'playing': + if currentsong['persistent ID'] != persistendId: + persistendId = currentsong['persistent ID'] + currentsong['timestamp'] = time.time() + (currentsong['artwork_url'], currentsong['itunes_url'], currentsong['artist_url']) = get_track_extras(currentsong['name'], currentsong['artist'], currentsong['album']) + printout(f"{post(currentsong)}") + timets = float(currentsong['duration'].replace(",", "."))-float(currentsong['pPosition'].replace(",", ".")) + 3 + prevstatus = 'playing' + elif currentsong['status'] == 'not playing' and prevstatus != 'not playing': + prevstatus = 'not playing' + printout(f"{post({'status' : 'not playing'})}") + timets = 5*60 + elif currentsong['status'] == 'not running' and prevstatus != 'not running': + prevstatus = 'not running' + printout(f"{post({'status' : 'not running'})}") + timets = 5*60 + else: + timets = 5*60 + + time.sleep(timets) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/user/install.sh b/user/install.sh new file mode 100755 index 0000000..86196d1 --- /dev/null +++ b/user/install.sh @@ -0,0 +1,9 @@ +#!/bin/sh -xe + +echo --- Copy launch agent plist +mkdir ~/Library/LaunchAgents/ || true +cp -f music-exp.plist ~/Library/LaunchAgents/ + +echo --- Load launch agent +launchctl load ~/Library/LaunchAgents/music-exp.plist +echo --- INSTALL SUCCESS diff --git a/user/music-exp.plist b/user/music-exp.plist new file mode 100644 index 0000000..01c5cbb --- /dev/null +++ b/user/music-exp.plist @@ -0,0 +1,24 @@ + + + + + KeepAlive + + Label + am.noh.music-exp + ProgramArguments + + + /Users/noham/miniconda3/envs/310/bin/python + export.py + + RunAtLoad + + StandardOutPath + logfile.log + StandardErrorPath + error_logfile.log + WorkingDirectory + /Users/noham/Documents/GitHub/AppleMusicApi/user + + diff --git a/user/test.py b/user/test.py index 078bbfa..72cad3b 100644 --- a/user/test.py +++ b/user/test.py @@ -10,7 +10,11 @@ USER = os.getenv("USER") PASSWORD = os.getenv("PASSWORD") def get_current_song(): - return subprocess.check_output(['osascript', 'test.applescript']).decode('utf-8').strip() + try: + output = subprocess.check_output(['osascript', 'export.applescript']).decode('utf-8').strip() + return output + except subprocess.CalledProcessError as e: + return e def get_track_extras(song, artist, album): query = f"{song} {artist} {album}" @@ -20,7 +24,6 @@ def get_track_extras(song, artist, album): json_data = r.json() if json_data["resultCount"] == 1: result = json_data["results"][0] - pprint(result) elif json_data["resultCount"] > 1: result = json_data["results"][0] else : @@ -29,7 +32,6 @@ def get_track_extras(song, artist, album): artwork_url = result["artworkUrl100"] if result else None itunes_url = result["trackViewUrl"] if result else None artist_url = result["artistViewUrl"] if result else None - # album_url = result["collectionViewUrl"] if result else None return (artwork_url, itunes_url, artist_url) @@ -43,8 +45,10 @@ def post(currentsong): else : return r.text -url = "http://192.168.1.58:3005" #RUN +url = "https://api.noh.am" #RUN WEB url = "http://127.0.0.1:5000" #DEV +url = "http://0.0.0.0:3005" + headers = {'Content-Type': 'application/json'} def main(): @@ -52,7 +56,8 @@ def main(): prevstatus = '' while True: print('getting data..') - currentsong = json.loads(str(get_current_song()).replace("'''", '"')) + # currentsong = json.loads(str(get_current_song()).replace("'''", '"')) + currentsong = json.loads(get_current_song()) if currentsong['status'] == 'playing': if currentsong['persistent ID'] != persistendId: diff --git a/user/uninstall.sh b/user/uninstall.sh new file mode 100755 index 0000000..1ed163f --- /dev/null +++ b/user/uninstall.sh @@ -0,0 +1,6 @@ +#!/bin/sh -xe +echo --- Unload launch agent +launchctl unload ~/Library/LaunchAgents/music-exp.plist +echo --- Remove launch agent plist +rm -f ~/Library/LaunchAgents/music-exp.plist || true +echo --- UNINSTALL SUCCESS