Compare commits

..

No commits in common. "909bd87383d2182dfbc7c9ec5134210b4b84b58b" and "00c0fd5189574009295f2c54294884849bb5278e" have entirely different histories.

4 changed files with 142 additions and 331 deletions

View file

@ -1,11 +1,8 @@
# 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. 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).
## 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.
@ -22,14 +19,21 @@ 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
uv pip install -r requirements.txt # Tai asenna manuaalisesti: spotipy requests python-dotenv flask gunicorn # TAI (Windows)
.venv\Scripts\activate
# Asenna riippuvuudet
uv pip install spotipy requests python-dotenv
``` ```
**2. Asetukset** **2. Asetukset**
Luo tiedosto nimeltä `.env` (voit kopioida `.env.example` -tiedoston pohjaksi) ja täytä sinne omat tietosi: Luo projektin juureen 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"
@ -41,51 +45,31 @@ 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"
``` ```
## Toimintatila 1: Komentorivi (Cron) **3. Ensimmäinen käynnistys ja Spotifyn luvitus**
Aja skripti kerran: Ensimmäisellä ajokerralla skriptin täytyy saada sinulta lupa muokata soittolistaasi.
Aja skripti terminaalissa:
```bash ```bash
python levyraati.py python skripti.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
0 4 * * * cd /polku/projektiin && .venv/bin/python levyraati.py >> loki.log 2>&1 crontab -e
``` ```
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
python levyraati.py --server 0 4 * * * cd /polku/projektiin && .venv/bin/python skripti.py >> skripti_loki.log 2>&1
``` ```
Palvelin kuuntelee portissa 5000. Webhook-osoite on `http://osoite:5000/webhook?secret=salainen_avain`. Tämä ajaa päivityksen joka aamu klo 04:00 ja tallentaa tulosteet `skripti_loki.log` -tiedostoon.
### 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`.

View file

@ -1,14 +0,0 @@
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,46 +1,30 @@
import os
import re
import datetime
import requests
import spotipy import spotipy
import sys
from spotipy.oauth2 import SpotifyOAuth from spotipy.oauth2 import SpotifyOAuth
import requests
import re
import os
import datetime
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
}
# --- WIKI-YHTEYS --- CLIENT_ID = os.getenv("SPOTIPY_CLIENT_ID")
CLIENT_SECRET = os.getenv("SPOTIPY_CLIENT_SECRET")
REDIRECT_URI = os.getenv("SPOTIPY_REDIRECT_URI")
PLAYLIST_ID = os.getenv("PLAYLIST_ID")
class WikiClient: WIKI_API_URL = os.getenv("WIKI_API_URL")
"""Luokka MediaWiki-rajapinnan kanssa toimimiseen.""" WIKI_PAGE_TITLE = os.getenv("WIKI_PAGE_TITLE")
WIKI_USERNAME = os.getenv("WIKI_USERNAME")
WIKI_PASSWORD = os.getenv("WIKI_PASSWORD")
def __init__(self, api_url, username, password): def hae_wiki_sisalto():
self.api_url = api_url """Hakee MediaWiki-sivun raakatekstin APIn kautta."""
self.username = username
self.password = password
self.session = requests.Session()
def kirjaudu(self): # Käytetään Session-oliota, jotta kirjautumiskeksit (cookies) pysyvät tallessa
"""Kirjautuu Wikiin ja palauttaa True jos onnistuu.""" session = requests.Session()
# Vaihe 1: Haetaan kirjautumistoken (login token)
token_params = { token_params = {
"action": "query", "action": "query",
"meta": "tokens", "meta": "tokens",
@ -49,237 +33,109 @@ class WikiClient:
} }
try: try:
# Vaihe 1: Haetaan kirjautumistoken response = session.get(url=WIKI_API_URL, params=token_params)
response = self.session.get(url=self.api_url, params=token_params) data = response.json()
response.raise_for_status() login_token = data['query']['tokens']['logintoken']
login_token = response.json()['query']['tokens']['logintoken']
# Vaihe 2: Kirjaudutaan sisään # Vaihe 2: Kirjaudutaan sisään tunnuksilla ja tokenilla (POST-pyyntö)
login_params = { login_params = {
"action": "login", "action": "login",
"lgname": self.username, "lgname": WIKI_USERNAME,
"lgpassword": self.password, "lgpassword": WIKI_PASSWORD,
"lgtoken": login_token, "lgtoken": login_token,
"format": "json" "format": "json"
} }
login_response = self.session.post(url=self.api_url, data=login_params) login_response = session.post(url=WIKI_API_URL, data=login_params)
login_response.raise_for_status() login_data = login_response.json()
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
print(f"Wikin kirjautuminen epäonnistui: {login_response.json()}") # Vaihe 3: Haetaan vihdoin itse sivun sisältö
return False page_params = {
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": otsikko, "titles": WIKI_PAGE_TITLE,
"format": "json" "format": "json"
} }
try: page_response = session.get(url=WIKI_API_URL, params=page_params)
response = self.session.get(url=self.api_url, params=params) page_data = page_response.json()
response.raise_for_status()
pages = response.json()["query"]["pages"] pages = page_data["query"]["pages"]
page_id = list(pages.keys())[0] page_id = list(pages.keys())[0]
if page_id == "-1": if page_id == "-1":
print(f"Virhe: Wiki-sivua '{otsikko}' ei löytynyt!") print("Virhe: Wiki-sivua 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 sivun noudossa: {e}") print(f"Virhe Wiki-yhteydessä: {e}")
return "" return ""
# --- TEKSTINKÄSITTELY --- def paivita_soittolista():
# 1. Haetaan teksti Wikistä
def erista_viikon_osio(teksti, viikko_numero): nykyinen_viikko_int = datetime.date.today().isocalendar()[1]
"""Eristää tietyn viikon osion koko sivun tekstistä (== Viikko X ==).""" nykyinen_viikko = f"{nykyinen_viikko_int:02d}"
viikko_str = f"{viikko_numero:02d}" print(f"Kuluva viikko on {nykyinen_viikko}")
pattern = rf"==\s*Viikko {viikko_str}\s*==(.*?(?===|\Z))" wiki_teksti = hae_wiki_sisalto()
match = re.search(pattern, teksti, re.IGNORECASE | re.DOTALL)
if match:
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():
linkit.append(url)
print(f"Löydetty Spotify-linkki: {url}")
else:
print(f"Ohitettu muu linkki: {url}")
return linkit
# --- SPOTIFY-PÄIVITYS ---
def paivita_spotify_soittolista(linkit, asetukset):
"""Päivittää Spotify-soittolistan annetuilla linkeillä."""
if not linkit:
print("\nEi päivitettäviä linkkejä. Soittolistaa ei muutettu.")
return False, "Ei linkkejä"
scope = "playlist-modify-private playlist-modify-public"
try:
sp = spotipy.Spotify(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
))
# Spotify sallii enintään 100 kappaletta per kutsu
sp.playlist_replace_items(asetukset["PLAYLIST_ID"], linkit[:100])
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: if not wiki_teksti:
return False, "Sivun haku epäonnistui" return
# 2. Oikean viikon osion eristäminen # 3. Eristetään kuluvan viikon osio tekstistä säännöllisellä lausekkeella (regex)
viikko_teksti = erista_viikon_osio(wiki_teksti, nykyinen_viikko) # Etsii esim. "== Viikko 10 ==" ja nappaa kaiken tekstin sen jälkeen
if not viikko_teksti: # aina seuraavaan "==" merkkiin asti tai sivun loppuun.
print(f"Huom: Viikon {nykyinen_viikko:02d} osiota ei löytynyt sivulta.") viikko_pattern = rf"==\s*Viikko {nykyinen_viikko}\s*==(.*?(?===|\Z))"
return False, f"Viikon {nykyinen_viikko} osiota ei löytynyt" match = re.search(viikko_pattern, wiki_teksti, re.IGNORECASE | re.DOTALL)
# 3. Linkkien kerääminen ja Spotifyn päivittäminen if not match:
spotify_linkit = poimi_spotify_linkit(viikko_teksti) print(f"\nWiki-sivulta ei löytynyt otsikkoa '== Viikko {nykyinen_viikko} =='. Soittolistaa ei päivitetty.")
success, msg = paivita_spotify_soittolista(spotify_linkit, asetukset) return
return success, msg
# --- FLASK PALVELIN --- kuluvan_viikon_teksti = match.group(1)
app = Flask(__name__) # 4. Poimitaan suorat Spotify-linkit koodista
track_urls = []
@app.route('/webhook', methods=['POST']) # Käydään wiki-teksti läpi rivi riviltä
def webhook(): for rivi in kuluvan_viikon_teksti.split('\n'):
asetukset = lataa_asetukset() # 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)
# Yksinkertainen turvatarkistus: vaaditaan salainen avain otsakkeessa tai parametrina if match:
secret = request.args.get('secret') or request.headers.get('X-Webhook-Secret') url = match.group(1) # Napataan pelkkä linkki
if secret != asetukset["WEBHOOK_SECRET"]: if "spotify.com" in url.lower():
return jsonify({"status": "error", "message": "Invalid secret"}), 403 track_urls.append(url)
print(f"Löydettiin linkki: {url}")
print("Webhook vastaanotettu! Käynnistetään päivitys...")
success, msg = aja_paivitys()
if success:
return jsonify({"status": "success", "message": msg}), 200
else: else:
return jsonify({"status": "error", "message": msg}), 500 print(f"Ohitettiin muun palvelun linkki: {url}")
# 5. Tarkistetaan löytyikö mitään
if not track_urls:
print("\nYhtään Spotify-linkkiä ei löytynyt Wiki-sivulta. Soittolistaa ei päivitetty.")
return
# 6. Kirjaudutaan sisään
scope = "playlist-modify-private playlist-modify-public"
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
redirect_uri=REDIRECT_URI,
scope=scope, open_browser=False))
# 7. Päivitetään soittolista
sp.playlist_replace_items(PLAYLIST_ID, track_urls[:100])
print("Soittolista päivitetty onnistuneesti!")
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) > 1 and sys.argv[1] == '--server': paivita_soittolista()
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()

View file

@ -1,15 +0,0 @@
[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