Compare commits

...

2 commits

4 changed files with 328 additions and 139 deletions

View file

@ -1,8 +1,11 @@
# Wiki-to-Spotify Soittolistan Päivittäjä 🎵 # Wiki-to-Spotify Soittolistan Päivittäjä 🎵
Tämä Python-skripti hakee automaattisesti kuluvan viikon biisivalinnat MediaWiki-sivulta ja päivittää ne haluttuun Spotifyn soittolistaan. Skripti on suunniteltu ajettavaksi automaattisesti taustalla (esim. Cronilla). Tämä Python-skripti hakee automaattisesti kuluvan viikon biisivalinnat MediaWiki-sivulta ja päivittää ne haluttuun Spotifyn soittolistaan.
## Ominaisuudet ## Ominaisuudet
* **Kaksi toimintatilaa:**
1. **Komentorivi:** Hakee biisit kerran ja lopettaa (sopii Cron-ajoon).
2. **Webhook-palvelin:** Kuuntelee (Flask/Gunicorn) ilmoituksia Wikistä ja päivittää soittolistan reaaliajassa kun sivu tallennetaan.
* Kirjautuu MediaWiki-rajapintaan ja lukee kuluvan viikon (esim. `== Viikko 10 ==`) sisällön. * Kirjautuu MediaWiki-rajapintaan ja lukee kuluvan viikon (esim. `== Viikko 10 ==`) sisällön.
* Etsii tekstistä suorat Spotify-linkit. * Etsii tekstistä suorat Spotify-linkit.
* Jättää huomiotta muiden palveluiden (esim. YouTube) linkit. * Jättää huomiotta muiden palveluiden (esim. YouTube) linkit.
@ -19,21 +22,14 @@ Tämä Python-skripti hakee automaattisesti kuluvan viikon biisivalinnat MediaWi
**1. Lataa koodi ja luo virtuaaliympäristö** **1. Lataa koodi ja luo virtuaaliympäristö**
```bash ```bash
# Luo virtuaaliympäristö uv:lla
uv venv uv venv
# Aktivoi ympäristö (Mac/Linux)
source .venv/bin/activate source .venv/bin/activate
# TAI (Windows) uv pip install -r requirements.txt # Tai asenna manuaalisesti: spotipy requests python-dotenv flask gunicorn
.venv\Scripts\activate
# Asenna riippuvuudet
uv pip install spotipy requests python-dotenv
``` ```
**2. Asetukset** **2. Asetukset**
Luo projektin juureen tiedosto nimeltä `.env` (voit kopioida `.env.example` -tiedoston pohjaksi) ja täytä sinne omat tietosi: Luo tiedosto nimeltä `.env` (voit kopioida `.env.example` -tiedoston pohjaksi) ja täytä sinne omat tietosi:
```env ```env
SPOTIPY_CLIENT_ID="sinun_spotify_client_id" SPOTIPY_CLIENT_ID="sinun_spotify_client_id"
@ -45,31 +41,51 @@ WIKI_USERNAME="wiki_tunnuksesi"
WIKI_PASSWORD="wiki_salasanasi" WIKI_PASSWORD="wiki_salasanasi"
WIKI_API_URL="https://sinun-wikisi.fi/api.php" WIKI_API_URL="https://sinun-wikisi.fi/api.php"
WIKI_PAGE_TITLE="Sivun_Nimi" WIKI_PAGE_TITLE="Sivun_Nimi"
# Valinnainen webhookin suojaus
WEBHOOK_SECRET="valitse_oma_salasana"
``` ```
**3. Ensimmäinen käynnistys ja Spotifyn luvitus** ## Toimintatila 1: Komentorivi (Cron)
Ensimmäisellä ajokerralla skriptin täytyy saada sinulta lupa muokata soittolistaasi. Aja skripti kerran:
Aja skripti terminaalissa:
```bash ```bash
python skripti.py python levyraati.py
``` ```
* Skripti tulostaa terminaaliin pitkän Spotify-linkin.
* Kopioi linkki ja avaa se selaimessasi.
* Kirjaudu sisään Spotifyhin ja hyväksy luvat.
* Selaimesi ohjautuu tyhjälle sivulle (localhost). Kopioi tuon sivun osoiterivillä oleva koko URL ja liitä se takaisin terminaaliin.
* Projektikansioosi syntyy `.cache` -tiedosto, joka pitää sinut jatkossa kirjautuneena. **Älä jaa tätä tiedostoa eteenpäin!**
## Automatisointi (Linux/Mac)
Voit asettaa skriptin pyörimään automaattisesti esimerkiksi kerran päivässä Cron-työkalulla:
Voit asettaa tämän ajumaan Cron-työkalulla esimerkiksi klo 04:00:
```bash ```bash
crontab -e 0 4 * * * cd /polku/projektiin && .venv/bin/python levyraati.py >> loki.log 2>&1
``` ```
Lisää tiedoston loppuun (muista muuttaa oikea polku):
## Toimintatila 2: Webhook-palvelin (Nginx + Gunicorn)
Tämä tila mahdollistaa reaaliaikaisen päivityksen kun Wikissä muokataan sivua.
### 1. Testaus manuaalisesti
```bash ```bash
0 4 * * * cd /polku/projektiin && .venv/bin/python skripti.py >> skripti_loki.log 2>&1 python levyraati.py --server
``` ```
Tämä ajaa päivityksen joka aamu klo 04:00 ja tallentaa tulosteet `skripti_loki.log` -tiedostoon. Palvelin kuuntelee portissa 5000. Webhook-osoite on `http://osoite:5000/webhook?secret=salainen_avain`.
### 2. Tuotantoasennus (Systemd)
1. Kopioi `levyraati.service` hakemistoon `/etc/systemd/system/`.
2. `sudo systemctl daemon-reload`
3. `sudo systemctl enable --now levyraati`
### 3. Nginx-konfiguraatio
1. Kopioi `levyraati.nginx` hakemistoon `/etc/nginx/sites-available/`.
2. Aktivoi ja käynnistä uudelleen: `sudo ln -s ... /etc/nginx/sites-enabled/ && sudo systemctl restart nginx`
## Spotifyn luvitus
Ensimmäisellä käyttökerralla (tai jos kirjautuminen vanhenee / token mitätöidään):
1. Aja terminaalissa:
```bash
python levyraati.py --auth
```
2. Skripti tulostaa Spotify-linkin. Avaa se selaimessasi ja hyväksy luvat.
3. Kopioi selaimen osoiterivillä oleva koko URL ja liitä se takaisin terminaaliin.
4. `.cache` -tiedosto syntyy ja pitää sinut kirjautuneena jatkossa.
Jos soittolistan päivityksessä tulee virhe `error: invalid_grant, error_description: Refresh token revoked`, aja uudelleen `python levyraati.py --auth`.

14
levyraati.nginx Normal file
View file

@ -0,0 +1,14 @@
server {
listen 80;
server_name _; # Vaihda tähän domainisi tai palvelimen IP-osoite
location /webhook {
include proxy_params;
proxy_pass http://unix:/run/levyraati/levyraati.sock;
}
# Kaikki muut pyynnöt palauttavat 404
location / {
return 404;
}
}

View file

@ -1,30 +1,46 @@
import spotipy
from spotipy.oauth2 import SpotifyOAuth
import requests
import re
import os import os
import re
import datetime import datetime
import requests
import spotipy
import sys
from spotipy.oauth2 import SpotifyOAuth
from dotenv import load_dotenv from dotenv import load_dotenv
from flask import Flask, request, jsonify
# --- KONFIGURAATIO ---
# Määritetään .cache-tiedoston polku aina skriptin omaan kansioon
CACHE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".cache")
def lataa_asetukset():
"""Lataa ympäristömuuttujat .env-tiedostosta."""
load_dotenv() load_dotenv()
return {
"SPOTIPY_CLIENT_ID": os.getenv("SPOTIPY_CLIENT_ID"),
"SPOTIPY_CLIENT_SECRET": os.getenv("SPOTIPY_CLIENT_SECRET"),
"SPOTIPY_REDIRECT_URI": os.getenv("SPOTIPY_REDIRECT_URI"),
"PLAYLIST_ID": os.getenv("PLAYLIST_ID"),
"WIKI_API_URL": os.getenv("WIKI_API_URL"),
"WIKI_PAGE_TITLE": os.getenv("WIKI_PAGE_TITLE"),
"WIKI_USERNAME": os.getenv("WIKI_USERNAME"),
"WIKI_PASSWORD": os.getenv("WIKI_PASSWORD"),
"WEBHOOK_SECRET": os.getenv("WEBHOOK_SECRET", "salainen_avain"), # Lisätty turva
}
CLIENT_ID = os.getenv("SPOTIPY_CLIENT_ID") # --- WIKI-YHTEYS ---
CLIENT_SECRET = os.getenv("SPOTIPY_CLIENT_SECRET")
REDIRECT_URI = os.getenv("SPOTIPY_REDIRECT_URI")
PLAYLIST_ID = os.getenv("PLAYLIST_ID")
WIKI_API_URL = os.getenv("WIKI_API_URL") class WikiClient:
WIKI_PAGE_TITLE = os.getenv("WIKI_PAGE_TITLE") """Luokka MediaWiki-rajapinnan kanssa toimimiseen."""
WIKI_USERNAME = os.getenv("WIKI_USERNAME")
WIKI_PASSWORD = os.getenv("WIKI_PASSWORD")
def hae_wiki_sisalto(): def __init__(self, api_url, username, password):
"""Hakee MediaWiki-sivun raakatekstin APIn kautta.""" self.api_url = api_url
self.username = username
self.password = password
self.session = requests.Session()
# Käytetään Session-oliota, jotta kirjautumiskeksit (cookies) pysyvät tallessa def kirjaudu(self):
session = requests.Session() """Kirjautuu Wikiin ja palauttaa True jos onnistuu."""
# Vaihe 1: Haetaan kirjautumistoken (login token)
token_params = { token_params = {
"action": "query", "action": "query",
"meta": "tokens", "meta": "tokens",
@ -33,109 +49,237 @@ def hae_wiki_sisalto():
} }
try: try:
response = session.get(url=WIKI_API_URL, params=token_params) # Vaihe 1: Haetaan kirjautumistoken
data = response.json() response = self.session.get(url=self.api_url, params=token_params)
login_token = data['query']['tokens']['logintoken'] response.raise_for_status()
login_token = response.json()['query']['tokens']['logintoken']
# Vaihe 2: Kirjaudutaan sisään tunnuksilla ja tokenilla (POST-pyyntö) # Vaihe 2: Kirjaudutaan sisään
login_params = { login_params = {
"action": "login", "action": "login",
"lgname": WIKI_USERNAME, "lgname": self.username,
"lgpassword": WIKI_PASSWORD, "lgpassword": self.password,
"lgtoken": login_token, "lgtoken": login_token,
"format": "json" "format": "json"
} }
login_response = session.post(url=WIKI_API_URL, data=login_params) login_response = self.session.post(url=self.api_url, data=login_params)
login_data = login_response.json() login_response.raise_for_status()
if login_data.get("login", {}).get("result") != "Success":
print(f"Wikin kirjautuminen epäonnistui: {login_data}")
return ""
if login_response.json().get("login", {}).get("result") == "Success":
print("Kirjauduttu Wikiin onnistuneesti!") print("Kirjauduttu Wikiin onnistuneesti!")
return True
# Vaihe 3: Haetaan vihdoin itse sivun sisältö print(f"Wikin kirjautuminen epäonnistui: {login_response.json()}")
page_params = { return False
except Exception as e:
print(f"Virhe Wiki-kirjautumisessa: {e}")
return False
def hae_sivun_sisalto(self, otsikko):
"""Hakee annetun sivun raakatekstin."""
params = {
"action": "query", "action": "query",
"prop": "revisions", "prop": "revisions",
"rvprop": "content", "rvprop": "content",
"rvslots": "main", "rvslots": "main",
"titles": WIKI_PAGE_TITLE, "titles": otsikko,
"format": "json" "format": "json"
} }
page_response = session.get(url=WIKI_API_URL, params=page_params) try:
page_data = page_response.json() response = self.session.get(url=self.api_url, params=params)
response.raise_for_status()
pages = page_data["query"]["pages"] pages = response.json()["query"]["pages"]
page_id = list(pages.keys())[0] page_id = list(pages.keys())[0]
if page_id == "-1": if page_id == "-1":
print("Virhe: Wiki-sivua ei löytynyt!") print(f"Virhe: Wiki-sivua '{otsikko}' ei löytynyt!")
return "" return ""
return pages[page_id]["revisions"][0]["slots"]["main"]["*"] return pages[page_id]["revisions"][0]["slots"]["main"]["*"]
except Exception as e: except Exception as e:
print(f"Virhe Wiki-yhteydessä: {e}") print(f"Virhe sivun noudossa: {e}")
return "" return ""
def paivita_soittolista(): # --- TEKSTINKÄSITTELY ---
# 1. Haetaan teksti Wikistä
nykyinen_viikko_int = datetime.date.today().isocalendar()[1]
nykyinen_viikko = f"{nykyinen_viikko_int:02d}"
print(f"Kuluva viikko on {nykyinen_viikko}")
wiki_teksti = hae_wiki_sisalto()
if not wiki_teksti:
return
# 3. Eristetään kuluvan viikon osio tekstistä säännöllisellä lausekkeella (regex) def erista_viikon_osio(teksti, viikko_numero):
# Etsii esim. "== Viikko 10 ==" ja nappaa kaiken tekstin sen jälkeen """Eristää tietyn viikon osion koko sivun tekstistä (== Viikko X ==)."""
# aina seuraavaan "==" merkkiin asti tai sivun loppuun. viikko_str = f"{viikko_numero:02d}"
viikko_pattern = rf"==\s*Viikko {nykyinen_viikko}\s*==(.*?(?===|\Z))" pattern = rf"==\s*Viikko {viikko_str}\s*==(.*?(?===|\Z))"
match = re.search(viikko_pattern, wiki_teksti, re.IGNORECASE | re.DOTALL) match = re.search(pattern, teksti, re.IGNORECASE | re.DOTALL)
if not match:
print(f"\nWiki-sivulta ei löytynyt otsikkoa '== Viikko {nykyinen_viikko} =='. Soittolistaa ei päivitetty.")
return
kuluvan_viikon_teksti = match.group(1)
# 4. Poimitaan suorat Spotify-linkit koodista
track_urls = []
# Käydään wiki-teksti läpi rivi riviltä
for rivi in kuluvan_viikon_teksti.split('\n'):
# Etsitään vain rivit, joissa lukee if '' in rivi:
# Säännöllinen lauseke (regex), joka etsii http- tai https-alkuisen
# linkin heti avaavan hakasulkeen [ jälkeen
match = re.search(r'\[(https?://[^\s\]]+)', rivi)
if match: if match:
url = match.group(1) # Napataan pelkkä linkki return match.group(1).strip()
return None
def poimi_spotify_linkit(teksti):
"""Etsii tekstistä Spotify-kappaleiden linkit hakasulkeiden sisältä."""
linkit = []
for rivi in teksti.split('\n'):
# Etsitään [http...] muotoisia linkkejä
match = re.search(r'\[(https?://[^\s\]]+)', rivi)
if match:
url = match.group(1)
if "spotify.com" in url.lower(): if "spotify.com" in url.lower():
track_urls.append(url) linkit.append(url)
print(f"Löydettiin linkki: {url}") print(f"Löydetty Spotify-linkki: {url}")
else: else:
print(f"Ohitettiin muun palvelun linkki: {url}") print(f"Ohitettu muu linkki: {url}")
return linkit
# 5. Tarkistetaan löytyikö mitään # --- SPOTIFY-PÄIVITYS ---
if not track_urls:
print("\nYhtään Spotify-linkkiä ei löytynyt Wiki-sivulta. Soittolistaa ei päivitetty.") def paivita_spotify_soittolista(linkit, asetukset):
return """Päivittää Spotify-soittolistan annetuilla linkeillä."""
if not linkit:
print("\nEi päivitettäviä linkkejä. Soittolistaa ei muutettu.")
return False, "Ei linkkejä"
# 6. Kirjaudutaan sisään
scope = "playlist-modify-private playlist-modify-public" scope = "playlist-modify-private playlist-modify-public"
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(client_id=CLIENT_ID, try:
client_secret=CLIENT_SECRET, sp = spotipy.Spotify(auth_manager=SpotifyOAuth(
redirect_uri=REDIRECT_URI, client_id=asetukset["SPOTIPY_CLIENT_ID"],
scope=scope, open_browser=False)) client_secret=asetukset["SPOTIPY_CLIENT_SECRET"],
redirect_uri=asetukset["SPOTIPY_REDIRECT_URI"],
scope=scope,
cache_path=CACHE_PATH,
open_browser=False
))
# 7. Päivitetään soittolista # Spotify sallii enintään 100 kappaletta per kutsu
sp.playlist_replace_items(PLAYLIST_ID, track_urls[:100]) sp.playlist_replace_items(asetukset["PLAYLIST_ID"], linkit[:100])
print("Soittolista päivitetty onnistuneesti!") print(f"Soittolista päivitetty onnistuneesti ({len(linkit[:100])} kappaletta).")
return True, f"Päivitetty {len(linkit[:100])} kappaletta"
except Exception as e:
err_msg = str(e)
if "invalid_grant" in err_msg or "Refresh token revoked" in err_msg:
print("\n[!] Spotifyn luvitus on vanhentunut tai mitätöity (Refresh token revoked).")
print(" Suorita uudelleenluvitus ajamalla terminaalissa:")
print(" source .venv/bin/activate && python levyraati.py --auth\n")
else:
print(f"Virhe Spotifyn päivityksessä: {e}")
return False, err_msg
def suorita_luvitus(asetukset):
"""Suorittaa Spotifyn OAuth-luvituksen interaktiivisesti terminaalissa."""
scope = "playlist-modify-private playlist-modify-public"
auth_manager = SpotifyOAuth(
client_id=asetukset["SPOTIPY_CLIENT_ID"],
client_secret=asetukset["SPOTIPY_CLIENT_SECRET"],
redirect_uri=asetukset["SPOTIPY_REDIRECT_URI"],
scope=scope,
cache_path=CACHE_PATH,
open_browser=False
)
# Poistetaan aiempi vanhentunut cache-tiedosto
if os.path.exists(CACHE_PATH):
try:
os.remove(CACHE_PATH)
print(f"Poistettu vanha luvitustiedosto ({CACHE_PATH}).")
except OSError as e:
print(f"Huom: vanhan luvitustiedoston poisto epäonnistui: {e}")
auth_url = auth_manager.get_authorize_url()
print("\n=================== SPOTIFYN LUVITUS ===================")
print("1. Avaa seuraava osoite selaimessasi:\n")
print(f" {auth_url}\n")
print("2. Kirjaudu Spotifyhin ja hyväksy sovelluksen pyytämät oikeudet.")
print("3. Selain ohjautuu osoitteeseen:")
print(f" {asetukset['SPOTIPY_REDIRECT_URI']}?code=...")
print(" (Vaikka selain näyttäisi virhesivua kuten 404, osoite on oikea!)")
print("4. Kopioi selaimen osoiteriviltä koko URL ja liitä se alle.")
print("========================================================\n")
try:
uudelleenohjattu_url = input("Liitä uudelleenohjattu URL tähän: ").strip()
if not uudelleenohjattu_url:
print("Virhe: URL oli tyhjä.")
return False
code = auth_manager.parse_response_code(uudelleenohjattu_url)
auth_manager.get_access_token(code, as_dict=False, check_cache=False)
print(f"\n[+] Token tallennettu onnistuneesti tiedostoon {CACHE_PATH}!")
# Testataan yhteys
sp = spotipy.Spotify(auth_manager=auth_manager)
kayttaja = sp.me()
print(f"[+] Spotify-yhteys toimii! Kirjautunut käyttäjä: {kayttaja.get('display_name')} ({kayttaja.get('id')})")
if asetukset.get("PLAYLIST_ID"):
soittolista = sp.playlist(asetukset["PLAYLIST_ID"])
print(f"[+] Pääsy soittolistaan kunnossa: '{soittolista.get('name')}'")
print("\nValmista! Taustapalvelin ja cron voivat nyt päivittää soittolistaa.")
return True
except Exception as e:
print(f"\n[-] Virhe luvituksessa: {e}")
return False
# --- PÄÄOHJELMA ---
def aja_paivitys():
"""Suorittaa koko päivitysprosessin vaihe kerrallaan."""
asetukset = lataa_asetukset()
nykyinen_viikko = datetime.date.today().isocalendar()[1]
print(f"--- Aloitetaan päivitys (Viikko {nykyinen_viikko:02d}) ---")
# 1. Wiki-yhteys ja sisällön haku
wiki = WikiClient(
asetukset["WIKI_API_URL"],
asetukset["WIKI_USERNAME"],
asetukset["WIKI_PASSWORD"]
)
if not wiki.kirjaudu():
return False, "Kirjautuminen epäonnistui"
wiki_teksti = wiki.hae_sivun_sisalto(asetukset["WIKI_PAGE_TITLE"])
if not wiki_teksti:
return False, "Sivun haku epäonnistui"
# 2. Oikean viikon osion eristäminen
viikko_teksti = erista_viikon_osio(wiki_teksti, nykyinen_viikko)
if not viikko_teksti:
print(f"Huom: Viikon {nykyinen_viikko:02d} osiota ei löytynyt sivulta.")
return False, f"Viikon {nykyinen_viikko} osiota ei löytynyt"
# 3. Linkkien kerääminen ja Spotifyn päivittäminen
spotify_linkit = poimi_spotify_linkit(viikko_teksti)
success, msg = paivita_spotify_soittolista(spotify_linkit, asetukset)
return success, msg
# --- FLASK PALVELIN ---
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
asetukset = lataa_asetukset()
# Yksinkertainen turvatarkistus: vaaditaan salainen avain otsakkeessa tai parametrina
secret = request.args.get('secret') or request.headers.get('X-Webhook-Secret')
if secret != asetukset["WEBHOOK_SECRET"]:
return jsonify({"status": "error", "message": "Invalid secret"}), 403
print("Webhook vastaanotettu! Käynnistetään päivitys...")
success, msg = aja_paivitys()
if success:
return jsonify({"status": "success", "message": msg}), 200
else:
return jsonify({"status": "error", "message": msg}), 500
if __name__ == '__main__': if __name__ == '__main__':
paivita_soittolista() if len(sys.argv) > 1 and sys.argv[1] == '--server':
print("Käynnistetään Webhook-palvelin portissa 5000...")
# Huom: Tuotannossa käytä WSGI-palvelinta (esim. gunicorn)
app.run(host='0.0.0.0', port=5000)
elif len(sys.argv) > 1 and sys.argv[1] in ('--auth', '--reauth'):
asetukset = lataa_asetukset()
suorita_luvitus(asetukset)
else:
aja_paivitys()

15
levyraati.service Normal file
View file

@ -0,0 +1,15 @@
[Unit]
Description=Gunicorn instance to serve levyraati webhook
After=network.target
[Service]
User=lamperi
Group=www-data
RuntimeDirectory=levyraati
WorkingDirectory=/home/lamperi/git-repos/e4-scriptit
Environment="PATH=/home/lamperi/git-repos/e4-scriptit/.venv/bin"
# Ladataan myös .env tiedosto jos tarpeen, mutta python-dotenv hoitaa sen jo
ExecStart=/home/lamperi/git-repos/e4-scriptit/.venv/bin/gunicorn --workers 3 --bind unix:/run/levyraati/levyraati.sock -m 007 levyraati:app
[Install]
WantedBy=multi-user.target