From fdccde6137f529ab0bc9a390a59449d89733ae79 Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Sat, 8 Feb 2025 13:25:40 +0100 Subject: [PATCH 01/22] refactor: comment out unused AutoIndex import and related code --- conjurer_musician/conjurer_musician.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index 11048c5..17129e9 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -1,9 +1,9 @@ -# This Python file uses the following encoding: utf-8 """ The provided Python script sets up a Flask web server to manage a list of music files, with functions for rescanning the music folder, updating the music list, and serving the music list via API endpoints. """ + import json import logging import os From 0e860fe6a9ba7a1dda616a0bcf19304f40dd48eb Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Wed, 9 Apr 2025 18:55:52 +0200 Subject: [PATCH 02/22] Add auth to ytdlp --- constants.py | 3 +++ music_functions.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/constants.py b/constants.py index 60cb612..322c2c3 100644 --- a/constants.py +++ b/constants.py @@ -120,6 +120,9 @@ SPOTIFY_CTRL = spotipy.Spotify( client_secret=authTokens[2], ) ) +REMOTE_HOST_NAME = "youtube" +authTokens = netrc_mod.authenticators(REMOTE_HOST_NAME) +YOUTUBE_AUTH = [authTokens[0],authTokens[2]] WORD_REACTIONS = DATA["word_reactions"] CYCLIC_WORDS = DATA["cyclic_words"] diff --git a/music_functions.py b/music_functions.py index 06bf7fe..1c2f263 100644 --- a/music_functions.py +++ b/music_functions.py @@ -16,6 +16,7 @@ from constants import ( MUSIC_FOLDER, SEND_MP3, SPOTIFY_CTRL, + YOUTUBE_AUTH, ) from spotify_dl import spotify from spotify_dl import youtube as youtube_download @@ -161,6 +162,7 @@ async def get_file(ctx, source, link): file_name_f=file_name_f, multi_core=0, proxy="", + YT_AUTH = YOUTUBE_AUTH ) _ = await coro logger.info("YT-DL done") @@ -203,6 +205,8 @@ async def get_file(ctx, source, link): else: dir_path = "/home/pi/RetroPie/mp3/Youtube" ydl_opts = { + "username": YOUTUBE_AUTH[0], + "password": YOUTUBE_AUTH[1], "proxy": "", "default_search": "ytsearch", "format": "bestaudio/best", From c103b6b22e1f333c626f976c34f96c0cdefff9b5 Mon Sep 17 00:00:00 2001 From: Michal T Date: Wed, 9 Apr 2025 18:21:37 +0100 Subject: [PATCH 03/22] Modified spotipy for auth --- spotify_dl/__init__.py | 6 ++-- spotify_dl/constants.py | 2 +- spotify_dl/spotify.py | 68 ++++++++++++++++++++++++++++++---------- spotify_dl/spotify_dl.py | 2 +- spotify_dl/youtube.py | 22 ++++++------- 5 files changed, 69 insertions(+), 31 deletions(-) diff --git a/spotify_dl/__init__.py b/spotify_dl/__init__.py index 907630e..3de80ef 100644 --- a/spotify_dl/__init__.py +++ b/spotify_dl/__init__.py @@ -8,5 +8,7 @@ def signal_handler(sig, frame): print("\nCaught interrupt(did you press Ctrl+C?), stopping spotify_dl") sys.exit(0) - -signal.signal(signal.SIGINT, signal_handler) +try: + signal.signal(signal.SIGINT, signal_handler) +except ValueError: + print("Exception in signal handler") diff --git a/spotify_dl/constants.py b/spotify_dl/constants.py index 1210085..d9ce9e0 100644 --- a/spotify_dl/constants.py +++ b/spotify_dl/constants.py @@ -3,7 +3,7 @@ from pathlib import Path __all__ = ["VERSION"] -VERSION = "8.8.1" +VERSION = "8.9.0" if os.getenv("XDG_CACHE_HOME") is not None: SAVE_PATH = os.getenv("XDG_CACHE_HOME") + "/spotifydl" diff --git a/spotify_dl/spotify.py b/spotify_dl/spotify.py index 7969a58..4ce8d58 100644 --- a/spotify_dl/spotify.py +++ b/spotify_dl/spotify.py @@ -1,15 +1,17 @@ + import sys +import logging from spotify_dl.scaffold import log from spotify_dl.utils import sanitize from rich.progress import Progress -def fetch_tracks(sp, item_type, url): +def fetch_tracks(sp, item_type, item_id): """ - Fetches tracks from the provided URL. + Fetches tracks from the provided item_id. :param sp: Spotify client :param item_type: Type of item being requested for: album/playlist/track - :param url: URL of the item + :param item_id: id of the item :return Dictionary of song and artist """ songs_list = [] @@ -17,11 +19,14 @@ def fetch_tracks(sp, item_type, url): songs_fetched = 0 if item_type == "playlist": + logger = logging.getLogger("discord") + + logger.info("Playlist") with Progress() as progress: songs_task = progress.add_task(description="Fetching songs from playlist..") while True: items = sp.playlist_items( - playlist_id=url, + playlist_id=item_id, fields="items.track.name,items.track.artists(name, uri)," "items.track.album(name, release_date, total_tracks, images)," "items.track.track_number,total, next,offset," @@ -30,6 +35,7 @@ def fetch_tracks(sp, item_type, url): offset=offset, ) total_songs = items.get("total") + logger.info(total_songs) track_info_task = progress.add_task( description="Fetching track info", total=len(items["items"]) ) @@ -37,16 +43,32 @@ def fetch_tracks(sp, item_type, url): track_info = item.get("track") # If the user has a podcast in their playlist, there will be no track # Without this conditional, the program will fail later on when the metadata is fetched + logger.info(item) if track_info is None: offset += 1 continue + if not track_info.get("artists")[0]["name"]: + offset +=1 + continue track_album_info = track_info.get("album") track_num = track_info.get("track_number") - spotify_id = track_info.get("id") track_name = track_info.get("name") - track_artist = ", ".join( - [artist["name"] for artist in track_info.get("artists")] - ) + spotify_id = track_info.get("id") + track_audio_data = "No audio data" + try: + track_audio_data = sp.audio_analysis(spotify_id) + tempo = track_audio_data.get("track").get("tempo") + except: + log.error("Couldn't fetch audio analysis for %s", track_name) + tempo = None + print(track_info.get("artists")) + print(track_audio_data) + track_artist = "" + for artist in track_info.get("artists"): + if artist["name"]: + track_artist = ", ".join( + [artist["name"]] + ) if track_album_info: track_album = track_album_info.get("name") track_year = ( @@ -86,6 +108,7 @@ def fetch_tracks(sp, item_type, url): "genre": genre, "spotify_id": spotify_id, "track_url": None, + "tempo": tempo, } ) offset += 1 @@ -111,8 +134,8 @@ def fetch_tracks(sp, item_type, url): description="Fetching songs from the album.." ) while True: - album_info = sp.album(album_id=url) - items = sp.album_tracks(album_id=url, offset=offset) + album_info = sp.album(album_id=item_id) + items = sp.album_tracks(album_id=item_id, offset=offset) total_songs = items.get("total") track_album = album_info.get("name") track_year = ( @@ -141,6 +164,12 @@ def fetch_tracks(sp, item_type, url): ) track_num = item["track_number"] spotify_id = item.get("id") + try: + track_audio_data = sp.audio_analysis(spotify_id) + tempo = track_audio_data.get("track").get("tempo") + except: + log.error("Couldn't fetch audio analysis for %s", track_name) + tempo = None songs_list.append( { "name": track_name, @@ -154,6 +183,7 @@ def fetch_tracks(sp, item_type, url): "cover": cover, "genre": genre, "spotify_id": spotify_id, + "tempo": tempo, } ) offset += 1 @@ -168,7 +198,7 @@ def fetch_tracks(sp, item_type, url): break elif item_type == "track": - items = sp.track(track_id=url) + items = sp.track(track_id=item_id) track_name = items.get("name") album_info = items.get("album") track_artist = ", ".join([artist["name"] for artist in items["artists"]]) @@ -182,6 +212,12 @@ def fetch_tracks(sp, item_type, url): album_total = album_info.get("total_tracks") track_num = items["track_number"] spotify_id = items["id"] + try: + track_audio_data = sp.audio_analysis(spotify_id) + tempo = track_audio_data.get("track").get("tempo") + except: + log.error("Couldn't fetch audio analysis for %s", track_name) + tempo = None if len(items["album"]["images"]) > 0: cover = items["album"]["images"][0]["url"] else: @@ -203,6 +239,7 @@ def fetch_tracks(sp, item_type, url): "genre": genre, "track_url": None, "spotify_id": spotify_id, + "tempo": tempo, } ) @@ -219,9 +256,10 @@ def parse_spotify_url(url): if url.startswith("spotify:"): log.error("Spotify URI was provided instead of a playlist/album/track URL.") sys.exit(1) - parsed_url = url.replace("https://open.spotify.com/", "").split("?")[0] - item_type = parsed_url.split("/")[0] - item_id = parsed_url.split("/")[1] + parsed_url = url.replace("https://open.spotify.com/", "").split("?")[0].split("/") + index_adjustment = 1 if parsed_url[0].startswith("intl") else 0 + item_type = parsed_url[0+index_adjustment] + item_id = parsed_url[1+index_adjustment] return item_type, item_id @@ -239,8 +277,6 @@ def get_item_name(sp, item_type, item_id): name = sp.album(album_id=item_id).get("name") elif item_type == "track": name = sp.track(track_id=item_id).get("name") - elif item_type == "artist_top_tracks": - name = sp.artist_top_tracks(artist_id=item_id).get("name") return sanitize(name) diff --git a/spotify_dl/spotify_dl.py b/spotify_dl/spotify_dl.py index d4e3d13..0add158 100644 --- a/spotify_dl/spotify_dl.py +++ b/spotify_dl/spotify_dl.py @@ -198,7 +198,7 @@ def spotify_dl(): ) url_dict["save_path"].mkdir(parents=True, exist_ok=True) log.info("Saving songs to %s directory", directory_name) - url_dict["songs"] = fetch_tracks(sp, item_type, url) + url_dict["songs"] = fetch_tracks(sp, item_type, item_id) url_data["urls"].append(url_dict.copy()) if args.dump_json is True: dump_json(url_dict["songs"]) diff --git a/spotify_dl/youtube.py b/spotify_dl/youtube.py index 3f132af..f1f8ef1 100644 --- a/spotify_dl/youtube.py +++ b/spotify_dl/youtube.py @@ -1,9 +1,9 @@ +import shutil import urllib.request from os import path import os import multiprocessing -import shutil import json import mutagen import csv @@ -65,13 +65,13 @@ def write_tracks(tracks_file, song_dict): i = 0 writer = csv.writer(file_out, delimiter=";") for url_dict in song_dict["urls"]: - # for track in url_dict['songs']: for track in url_dict["songs"]: track_url = track["track_url"] # here track_name = track["name"] track_artist = track["artist"] track_num = track["num"] track_album = track["album"] + track_tempo = track["tempo"] track["save_path"] = url_dict["save_path"] track_db.append(track) track_index = i @@ -82,6 +82,7 @@ def write_tracks(tracks_file, song_dict): track_url, str(track_num), track_album, + str(track_tempo), str(track_index), ] try: @@ -97,8 +98,8 @@ def write_tracks(tracks_file, song_dict): def set_tags(temp, filename, kwargs): """ sets song tags after they are downloaded - :param temp: contains index used to obtain more info about song being edited - :param filename: location of song whose tags are to be edited + :param temp: contains index used to obtain more info about song being editted + :param filename: location of song whose tags are to be editted :param kwargs: a dictionary of extra arguments to be used in tag editing """ song = kwargs["track_db"][int(temp[-1])] @@ -120,6 +121,8 @@ def set_tags(temp, filename, kwargs): ) song_file["genre"] = song.get("genre") + if song.get("tempo") is not None: + song_file["bpm"] = str(song.get("tempo")) song_file.save() song_file = MP3(filename, ID3=ID3) cover = song.get("cover") @@ -162,7 +165,7 @@ def find_and_download_songs(kwargs): print(f"Initiating download for {query}.") file_name = kwargs["file_name_f"]( - name=name, artist=artist, track_num=kwargs["track_db"][i].get("num") + name=name, artist=artist, track_num=kwargs["track_db"][i].get("playlist_num") ) if kwargs["use_sponsorblock"][0].lower() == "y": @@ -197,9 +200,10 @@ def find_and_download_songs(kwargs): ): print(f"File {mp3file_path} already exists, we do not overwrite it ") continue - outtmpl = f"{file_path}.%(ext)s" ydl_opts = { + "username":kwargs["YT_AUTH"][0], + "password":kwargs["YT_AUTH"][1], "proxy": kwargs.get("proxy"), "default_search": "ytsearch", "format": "bestaudio/best", @@ -312,11 +316,7 @@ def download_songs(**kwargs): log.debug("Downloading to %s", url["save_path"]) reference_file = DOWNLOAD_LIST track_db = write_tracks(reference_file, kwargs["songs"]) - try: - shutil.copy(reference_file, kwargs["output_dir"] + "/" + reference_file) - os.remove(reference_file) - except: - os.rename(reference_file, kwargs["output_dir"] + "/" + reference_file) + shutil.copy(reference_file, kwargs["output_dir"] + "/" + reference_file) reference_file = str(kwargs["output_dir"]) + "/" + reference_file kwargs["reference_file"] = reference_file kwargs["track_db"] = track_db From b4fe6270a9645eafc900714ac0d7c14587852a3f Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Thu, 17 Apr 2025 20:44:39 +0200 Subject: [PATCH 04/22] Fixes after reinstall --- constants.py | 2 +- install_main_bot.sh | 20 +++++++++++--------- requirements_bot.txt | 2 +- thin_client.py | 4 ++-- 4 files changed, 15 insertions(+), 13 deletions(-) mode change 100644 => 100755 thin_client.py diff --git a/constants.py b/constants.py index 322c2c3..d24a850 100644 --- a/constants.py +++ b/constants.py @@ -81,7 +81,7 @@ if platform in ("linux", "linux2"): MUSIC_FOLDER = "/home/pi/RetroPie/mp3/" SETTINGS_FILE = "/home/pi/Conjurer/settings.json" NETRC_FILE = "/home/pi/.netrc" - LOGSTORE = "/home/pi/RetroPie/logs/" + LOGSTORE = "/home/pi/Conjurer/logs/" ACCIDENT_LOG = "/home/pi/Conjurer/accident_log.json" ENCODING = "utf-8" GRAPHICS_PATH = "/home/pi/RetroPie/Conjurer_graphics/" diff --git a/install_main_bot.sh b/install_main_bot.sh index ac58dcb..33c4b22 100755 --- a/install_main_bot.sh +++ b/install_main_bot.sh @@ -1,20 +1,22 @@ #!/bin/bash sudo apt-get install python3-dev +sudo apt-get install portaudio19-dev python3-pyaudio +sudo apt-get install cd /home/pi || exit mdkir Conjurer cd Conjurer ||exit -python3 -m venv /home/pi/Conjurer/.venv +python3 -m venv /home/pi/Conjurer/.env cp /home/pi/conjurer/requirements_bot.txt /home/pi/Conjurer/ # trunk-ignore(shellcheck/SC1091) -source /home/pi/Conjurer/.venv/bin/activate -./env/bin/python3 -m pip install --upgrade pip -./env/bin/python3 -m pip install -r requirements_bot.txt -sed -i -e 's/os.rename/shutil.copy/g' ./env/lib/python3.11/site-packages/spotify_dl/youtube.py -sed -i '1i\import shutil' ./env/lib/python3.11/site-packages/spotify_dl/youtube.py -sed -i '1i\import logging' ./env/lib/python3.11/site-packages/spotify_dl/spotify.py +source /home/pi/Conjurer/.env/bin/activate +./.env/bin/python3 -m pip install --upgrade pip +./.env/bin/python3 -m pip install -r requirements_bot.txt +sed -i -e 's/os.rename/shutil.copy/g' ./.env/lib/python3.11/site-packages/spotify_dl/youtube.py +sed -i '1i\import shutil' ./.env/lib/python3.11/site-packages/spotify_dl/youtube.py +sed -i '1i\import logging' ./.env/lib/python3.11/site-packages/spotify_dl/spotify.py -sed -i '21i\ logger = logging.getLogger("discord")' ./env/lib/python3.11/site-packages/spotify_dl/spotify.py -sed -i '22i\ logger.info("Playlist")' ./env/lib/python3.11/site-packages/spotify_dl/spotify.py +sed -i '21i\ logger = logging.getLogger("discord")' ./.env/lib/python3.11/site-packages/spotify_dl/spotify.py +sed -i '22i\ logger.info("Playlist")' ./.env/lib/python3.11/site-packages/spotify_dl/spotify.py #add sed changing signal registration to catch exception in spotify dl init deactivate sudo cp ./conjurer.service /etc/systemd/system/ diff --git a/requirements_bot.txt b/requirements_bot.txt index 2831a78..9eeca53 100644 --- a/requirements_bot.txt +++ b/requirements_bot.txt @@ -17,4 +17,4 @@ waitress clickupython assemblyai[extras] SpeechRecognition -git+https://github.com/imayhaveborkedit/discord-ext-voice-recvO \ No newline at end of file +git+https://github.com/imayhaveborkedit/discord-ext-voice-recv diff --git a/thin_client.py b/thin_client.py old mode 100644 new mode 100755 index cbb1c33..efac6cb --- a/thin_client.py +++ b/thin_client.py @@ -75,8 +75,8 @@ async def on_ready(): for com in client.commands: logger.info("Command %s is awejleble", com.qualified_name) - logger.info("Logged in as ---->", client.user) - logger.info("ID:", client.user.id) + logger.info("Logged in as ----> %s", client.user) + logger.info("ID:%s ", client.user.id) logger.info("All systems: operational") From 88fdf35dc5d13a0a0f5891b2545856c8c2258828 Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Thu, 17 Apr 2025 21:51:30 +0200 Subject: [PATCH 05/22] fixx --- music_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/music_functions.py b/music_functions.py index 1c2f263..a6e2881 100644 --- a/music_functions.py +++ b/music_functions.py @@ -262,7 +262,7 @@ async def get_file(ctx, source, link): ] # TODO: Make sponsorblock work sponsorblock_postprocessor = [] - dir_path = "/home/pi/RetroPie/movies/porn" + dir_path = "/home/pi/MediaShare/movies/porn" ydl_opts = { "postprocessors": sponsorblock_postprocessor, "paths": {"home": dir_path}, From 7f560decd3dfc89f46f1b419319619307aaac084 Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Thu, 17 Apr 2025 22:59:47 +0200 Subject: [PATCH 06/22] Add musician fixes --- conjurer_musician/conjurer_musician.py | 6 +++--- conjurer_musician/requirements_musician.txt | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index 17129e9..16e18c4 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -48,10 +48,10 @@ if platform in ("linux", "linux2"): else: LOGFILE = "/home/pi/Conjurer/discord_mus_service.log" NETRC_FILE = "/home/pi/.netrc" - LOGSTORE = "/home/pi/RetroPie/logs/" + LOGSTORE = "/home/pi/MediaShare/logs/" ENCODING = "utf-8" - MUSIC_FOLDER = "/home/pi/RetroPie/mp3/" - PRIORITY_FOLDER = "/home/pi/RetroPie/mp3/Magiczne i chuj/" + MUSIC_FOLDER = "/home/pi/MediaShare/mp3/" + PRIORITY_FOLDER = "/home/pi/MediaShare/mp3/Magiczne i chuj/" RADIOLOG_PATH = "/home/pi/Conjurer/radio_log.log" PERSISTENCE_PATH = "/home/pi/Conjurer/persistence.log" diff --git a/conjurer_musician/requirements_musician.txt b/conjurer_musician/requirements_musician.txt index e298c25..43eb53b 100644 --- a/conjurer_musician/requirements_musician.txt +++ b/conjurer_musician/requirements_musician.txt @@ -1,3 +1,4 @@ flask waitress -sshtunnel \ No newline at end of file +sshtunnel +requests From b9e0f7353476a9c6c24170d9868bf5068e4a66b0 Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Fri, 18 Apr 2025 13:44:34 +0200 Subject: [PATCH 07/22] Fix in default config --- constants.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/constants.py b/constants.py index d24a850..ee4b1bd 100644 --- a/constants.py +++ b/constants.py @@ -78,14 +78,14 @@ if platform in ("linux", "linux2"): MEMORY_FIVE_SIARA = "/home/pi/Conjurer/pamiec.json" SYSTEM_GPT_SETTINGS = "/home/pi/Conjurer/system_gpt_settings.json" MEMORY_FIVE_MUZYKA = "/home/pi/Conjurer/pamiec_muzyki.json" - MUSIC_FOLDER = "/home/pi/RetroPie/mp3/" + MUSIC_FOLDER = "/home/pi/MediaShare/mp3/" SETTINGS_FILE = "/home/pi/Conjurer/settings.json" NETRC_FILE = "/home/pi/.netrc" - LOGSTORE = "/home/pi/Conjurer/logs/" + LOGSTORE = "/home/pi/MediaShara/logs/" ACCIDENT_LOG = "/home/pi/Conjurer/accident_log.json" ENCODING = "utf-8" - GRAPHICS_PATH = "/home/pi/RetroPie/Conjurer_graphics/" - DIR_PATH_SADOX = "/home/pi/RetroPie/Fansadox/" + GRAPHICS_PATH = "/home/pi/MediaShare/Conjurer_graphics/" + DIR_PATH_SADOX = "/home/pi/MediaShare/Fansadox/" elif platform == "win32": From f56935e48b3ffbd3f89930e64e9fc41b8c949032 Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Sat, 19 Apr 2025 17:14:53 +0200 Subject: [PATCH 08/22] How to setup radio --- ffmpeg.pref | 3 ++ manual_set_up_radio.txt | 101 ++++++++++++++++++++++++++++++++++++++++ pulseaudio.service | 11 +++++ 3 files changed, 115 insertions(+) create mode 100644 ffmpeg.pref create mode 100644 manual_set_up_radio.txt create mode 100644 pulseaudio.service diff --git a/ffmpeg.pref b/ffmpeg.pref new file mode 100644 index 0000000..eb72eeb --- /dev/null +++ b/ffmpeg.pref @@ -0,0 +1,3 @@ +Package: ffmpeg libavcodec-dev libavcodec59 libavdevice59 libavfilter8 libavformat-dev libavformat59 libavutil-dev libavutil57 libpostproc56 libswresample-dev libswresample4 libswscale-dev libswscale6 libavdevice-dev libavfilter-dev libpostproc-dev +Pin: origin deb.debian.org +Pin-Priority: 1001 diff --git a/manual_set_up_radio.txt b/manual_set_up_radio.txt new file mode 100644 index 0000000..6b825f9 --- /dev/null +++ b/manual_set_up_radio.txt @@ -0,0 +1,101 @@ +# === SYSTEM SETUP === +sudo apt update && sudo apt install -y \ + pulseaudio pulseaudio-utils pavucontrol \ + libgtk-3-dev gtk-3-examples \ + xauth dbus-x11 \ + samba + +# === SAMBA CONFIGURATION === +sudo nano /etc/samba/smb.conf +# Add at the end of the file: +# [RaspberryPiNAS] +# path = /home/pi/Public +# comment = Pi Share +# browseable = yes +# writeable = yes +# guest ok = no +# valid users = pi + +# Set permissions +chmod 755 /home/pi +mkdir -p /home/pi/Public +chmod 777 /home/pi/Public +chown -R pi:pi /home/pi/Public + +# Set Samba password for pi +sudo smbpasswd -a pi + +# Restart Samba +sudo systemctl restart smbd + +# === PULSEAUDIO SYSTEM-WIDE SERVICE === +sudo nano /etc/systemd/system/pulseaudio.service +# Paste this content: +# [Unit] +# Description=PulseAudio System-wide Daemon +# After=sound.target network.target +# +# [Service] +# Type=simple +# ExecStart=/usr/bin/pulseaudio --system --disallow-exit --disallow-module-loading=0 --daemonize=no +# Restart=always +# +# [Install] +# WantedBy=multi-user.target + +# === PULSEAUDIO MODULES FOR LEXICON LAMBDA USB === +sudo nano /etc/pulse/system.pa +# Comment out: +# load-module module-udev-detect +# load-module module-detect +# Add this at the bottom: +# load-module module-alsa-sink device=hw:1,0 sink_name=LambdaOutput sink_properties=device.description="Lexicon_Lambda_USB_Output" +# load-module module-alsa-source device=hw:1,0 source_name=LambdaInput source_properties=device.description="Lexicon_Lambda_USB_Input" +# load-module module-native-protocol-unix auth-anonymous=1 socket=/tmp/pulseaudio.socket + +# Set client default socket +sudo nano /etc/pulse/client.conf +# Add this line: +# default-server = unix:/tmp/pulseaudio.socket + +# Enable and start PulseAudio system-wide +sudo systemctl daemon-reload +sudo systemctl enable pulseaudio.service +sudo systemctl restart pulseaudio.service + +# === X11 & GUI FIXES FOR SSH/MOBAXTERM === +sudo nano /etc/ssh/sshd_config +# Ensure these lines are present: +# X11Forwarding yes +# X11UseLocalhost no +# XAuthLocation /usr/bin/xauth + +# Restart SSH +sudo systemctl restart ssh + +# Ensure xauth is installed +sudo apt install -y xauth + +# === BASHRC: MAKE GUI EXPORTS PERSISTENT === +nano ~/.bashrc +# Add this at the end: +# export GDK_BACKEND=x11 +# export LIBGL_ALWAYS_INDIRECT=1 +# export NO_AT_BRIDGE=1 +# export $(dbus-launch) + +# Reload bashrc immediately +source ~/.bashrc + +# === TESTING GUI === +# (Reconnect via MobaXterm SSH with X11 forwarding enabled before running below) + +# Run pavucontrol GUI +pavucontrol & + +# Optional: Test GTK GUI rendering +gtk3-demo & + +# Check audio devices +pactl list sinks short +pactl list sources short diff --git a/pulseaudio.service b/pulseaudio.service new file mode 100644 index 0000000..e8362dc --- /dev/null +++ b/pulseaudio.service @@ -0,0 +1,11 @@ +[Unit] +Description=PulseAudio System-wide Daemon +After=sound.target network.target + +[Service] +Type=simple +ExecStart=/usr/bin/pulseaudio --system --disallow-exit --disallow-module-loading=0 --daemonize=no +Restart=always + +[Install] +WantedBy=multi-user.target From 0297527965034da6c1829bcacc895a82734e111f Mon Sep 17 00:00:00 2001 From: mtuszowski Date: Sat, 19 Apr 2025 17:16:04 +0200 Subject: [PATCH 09/22] install manual --- install_music_bot.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/install_music_bot.sh b/install_music_bot.sh index a93163a..f1a984c 100755 --- a/install_music_bot.sh +++ b/install_music_bot.sh @@ -25,8 +25,12 @@ sudo usermod -aG pulse-access root sudo usermod -aG audio pi sudo usermod -aG audio root - - +echo "Add exception suppresion to signal handler in spotify __ini__.py" +echo "Add password to youtube opts in spotify_dl youtube.py" +echo "Downgrade ffmpeg by copying ffmpeg.pref from repository to /etc/apt/preferences.d" +echo "Install opam from installation link" +echo "Initialize opam" +echo "Install liquidsoap and its dependencies" touch /home/pi/Conjurer/radio_log.log ./env/bin/python3 -m pip install --upgrade pip From 95cd4760b7fbf704a2d1ecc66f5c0faa59d17da9 Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Sat, 26 Apr 2025 13:18:02 +0200 Subject: [PATCH 10/22] Generated scripts --- conjurer_musician/revoke_shares.py | 99 +++++++++++++++++++++++++++ conjurer_musician/scan_shares.py | 44 ++++++++++++ conjurer_musician/scan_shares.service | 6 ++ conjurer_musician/scan_shares.timer | 11 +++ conjurer_musician/search_and_share.py | 93 +++++++++++++++++++++++++ 5 files changed, 253 insertions(+) create mode 100755 conjurer_musician/revoke_shares.py create mode 100755 conjurer_musician/scan_shares.py create mode 100644 conjurer_musician/scan_shares.service create mode 100644 conjurer_musician/scan_shares.timer create mode 100755 conjurer_musician/search_and_share.py diff --git a/conjurer_musician/revoke_shares.py b/conjurer_musician/revoke_shares.py new file mode 100755 index 0000000..c68822b --- /dev/null +++ b/conjurer_musician/revoke_shares.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +import os +import re +import time +import json +import argparse +import sys +from pathlib import Path + +# — CONFIGURATION — adjust as needed — +APACHE_LOG = '/var/log/apache2/share_access.log' # <- point to your share-specific log! +SHARE_DIR = Path('/var/www/html/share') +METADATA_FILE = SHARE_DIR / '.downloads.json' +STATE_FILE = SHARE_DIR / '.logpos' +# Regex to match lines like: "GET /share/abcdef1234... HTTP/1.1" +TOKEN_REGEX = re.compile(r'"\s*GET\s+/share/([0-9a-f]{32})\s+HTTP/') + +def debug(msg, args): + if args.debug: + print(f"[DEBUG] {msg}", file=sys.stderr) + +def load_metadata(args): + if METADATA_FILE.exists(): + data = json.loads(METADATA_FILE.read_text()) + debug(f"Loaded metadata: {data}", args) + return data + return {} + +def save_metadata(md, args): + METADATA_FILE.write_text(json.dumps(md)) + debug(f"Saved metadata: {md}", args) + +def load_state(args): + if STATE_FILE.exists(): + pos = int(STATE_FILE.read_text()) + debug(f"Loaded last log position: {pos}", args) + return pos + return 0 + +def save_state(pos, args): + STATE_FILE.write_text(str(pos)) + debug(f"Saved new log position: {pos}", args) + +def scan_log(since_pos, args): + try: + with open(APACHE_LOG, 'r') as f: + f.seek(since_pos) + lines = f.readlines() + newpos = f.tell() + debug(f"Read {len(lines)} new lines", args) + return lines, newpos + except Exception as e: + print(f"Error reading log: {e}", file=sys.stderr) + return [], since_pos + +def record_downloads(lines, metadata, args): + now = time.time() + for line in lines: + m = TOKEN_REGEX.search(line) + if m: + tok = m.group(1) + if tok not in metadata: + metadata[tok] = now + debug(f"Recorded download of token {tok} at {now}", args) + return metadata + +def revoke_old(metadata, args): + now = time.time() + to_remove = [tok for tok, ts in metadata.items() if now - ts >= 120] + for tok in to_remove: + link = SHARE_DIR / tok + try: + link.unlink() + debug(f"Unlinked token {tok}", args) + except FileNotFoundError: + debug(f"Link {link} not found when revoking", args) + metadata.pop(tok, None) + return bool(to_remove) + +def main(): + p = argparse.ArgumentParser(description="Revoke share links 2min after download") + p.add_argument('--debug', action='store_true', help='print debug info to stderr') + args = p.parse_args() + + SHARE_DIR.mkdir(parents=True, exist_ok=True) + metadata = load_metadata(args) + state = load_state(args) + + lines, newpos = scan_log(state, args) + metadata = record_downloads(lines, metadata, args) + save_metadata(metadata, args) # always persist new downloads + + if revoke_old(metadata, args): + save_metadata(metadata, args) # persist removals + + save_state(newpos, args) + +if __name__ == '__main__': + main() diff --git a/conjurer_musician/scan_shares.py b/conjurer_musician/scan_shares.py new file mode 100755 index 0000000..dd78636 --- /dev/null +++ b/conjurer_musician/scan_shares.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +import os +import json +import time +from pathlib import Path + +# Configuration +SCAN_PATH = '/mnt/shares' +OUTPUT_FILE = '/var/log/share_scan.json' + +def scan_directory(root): + """Recursively walk `root` and collect metadata.""" + entries = [] + for dirpath, dirs, files in os.walk(root): + for name in dirs + files: + full = os.path.join(dirpath, name) + try: + stat = os.stat(full) + entries.append({ + 'path': full, + 'is_dir': os.path.isdir(full), + 'size_bytes': stat.st_size, + 'mtime': time.strftime('%Y-%m-%dT%H:%M:%S%z', + time.localtime(stat.st_mtime)), + }) + except Exception as e: + # skip items we can't stat + continue + return entries + +def main(): + data = { + 'scanned_at': time.strftime('%Y-%m-%dT%H:%M:%S%z', time.localtime()), + 'root': SCAN_PATH, + 'entries': scan_directory(SCAN_PATH), + } + # Write atomically + temp = OUTPUT_FILE + '.tmp' + with open(temp, 'w') as f: + json.dump(data, f, indent=2) + os.replace(temp, OUTPUT_FILE) + +if __name__ == '__main__': + main() diff --git a/conjurer_musician/scan_shares.service b/conjurer_musician/scan_shares.service new file mode 100644 index 0000000..861c100 --- /dev/null +++ b/conjurer_musician/scan_shares.service @@ -0,0 +1,6 @@ +[Unit] +Description=One‑shot scan of /mnt/shares → JSON + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/scan_shares.py diff --git a/conjurer_musician/scan_shares.timer b/conjurer_musician/scan_shares.timer new file mode 100644 index 0000000..3196c32 --- /dev/null +++ b/conjurer_musician/scan_shares.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Hourly timer for share‑scanner + +[Timer] +# delay 2min after boot, then every hour +OnBootSec=20min +OnUnitActiveSec=24h +Unit=scan_shares.service + +[Install] +WantedBy=timers.target diff --git a/conjurer_musician/search_and_share.py b/conjurer_musician/search_and_share.py new file mode 100755 index 0000000..c41160d --- /dev/null +++ b/conjurer_musician/search_and_share.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import sys +import uuid +from pathlib import Path + +# CONFIGURATION +JSON_DB = '/var/log/share_scan.json' +SHARE_DIR = Path('/var/www/html/share') +BASE_URL = 'https://czernobog.pl/share' + +# Ensure share directory exists +SHARE_DIR.mkdir(parents=True, exist_ok=True) + +def load_db(): + with open(JSON_DB) as f: + return json.load(f)['entries'] + +def relevancy(path, keywords): + score = 0 + low = path.lower() + for kw in keywords: + if kw.lower() in low: + score += low.count(kw.lower()) + return score + +def find_matches(entries, keywords): + scored = [] + for e in entries: + score = relevancy(e['path'], keywords) + if score > 0: + scored.append((score, e['path'])) + scored.sort(reverse=True, key=lambda x: x[0]) + return [p for _, p in scored] + +def interactive_select(candidates): + print("Search results:") + for idx, p in enumerate(candidates, 1): + print(f" {idx:3d}. {p}") + sel = input("\nSelect files (e.g. 1,3-5): ").strip() + nums = set() + for part in sel.split(','): + if '-' in part: + a,b = part.split('-',1) + nums.update(range(int(a), int(b)+1)) + else: + nums.add(int(part)) + return [candidates[i-1] for i in sorted(nums) if 1 <= i <= len(candidates)] + +def publish(paths): + urls = [] + for path in paths: + token = uuid.uuid4().hex + link = SHARE_DIR / token + try: + os.symlink(path, link) + except FileExistsError: + pass + urls.append(f"{BASE_URL}/{token}") + return urls + +def main(): + parser = argparse.ArgumentParser(description="Search and share files from JSON DB") + parser.add_argument('keywords', nargs='*', + help='Search keywords (interactive if omitted)') + parser.add_argument('-n','--count', type=int, default=10, + help='How many top results to share') + args = parser.parse_args() + + if not args.keywords: + # Interactive: ask for query + q = input("Enter search keywords (space-separated): ").strip().split() + args.keywords = q + + entries = load_db() + matches = find_matches(entries, args.keywords) + top_n = matches[:args.count] + + if sys.stdin.isatty(): + # interactive selection + sel = interactive_select(top_n) + else: + # non-interactive: share all top_n + sel = top_n + + urls = publish(sel) + for u in urls: + print(u) + +if __name__=='__main__': + main() From a8001ec19dad58e41c4da2e038e9b0ddf6c51fa9 Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Fri, 2 May 2025 19:01:51 +0200 Subject: [PATCH 11/22] new function --- .../radio_conjurer.etc.logrotate | 9 +++ file_search_commqnds.py | 76 +++++++++++++++++++ file_search_functions.py | 51 +++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 conjurer_musician/radio_conjurer.etc.logrotate create mode 100644 file_search_commqnds.py create mode 100644 file_search_functions.py diff --git a/conjurer_musician/radio_conjurer.etc.logrotate b/conjurer_musician/radio_conjurer.etc.logrotate new file mode 100644 index 0000000..ea879af --- /dev/null +++ b/conjurer_musician/radio_conjurer.etc.logrotate @@ -0,0 +1,9 @@ +/home/pi/Conjurer/radio_log.log { + daily + rotate 7 + compress + delaycompress + missingok + notifempty + copytruncate +} diff --git a/file_search_commqnds.py b/file_search_commqnds.py new file mode 100644 index 0000000..0f09bc7 --- /dev/null +++ b/file_search_commqnds.py @@ -0,0 +1,76 @@ +import os +import discord +from discord.ext import commands +import file_search_functions + +class FileSelectView(discord.ui.View): + def __init__(self, files): + super().__init__(timeout=60) + # Create select options for each file + options = [discord.SelectOption(label=os.path.basename(f)[:100], description=f, value=f) for f in files] + self.select = discord.ui.Select( + placeholder="Select files...", + min_values=1, + max_values=len(options), + options=options + ) + self.select.callback = self.select_callback + self.add_item(self.select) + self.selected = [] + + async def select_callback(self, interaction: discord.Interaction): + # Store selected file paths + self.selected = self.select.values + await interaction.response.defer() + + @discord.ui.button(label="Accept", style=discord.ButtonStyle.green) + async def accept_callback(self, button: discord.ui.Button, interaction: discord.Interaction): + # Handle acceptance + if not self.selected: + await interaction.response.send_message("No files selected.", ephemeral=True) + return + # Publish selected files + links = file_search_functions.publish(self.selected) + # Display the returned links + await interaction.response.edit_message(content="Published Links:\n" + "\n".join(links), view=None) + + @discord.ui.button(label="Cancel", style=discord.ButtonStyle.red) + async def cancel_callback(self, button: discord.ui.Button, interaction: discord.Interaction): + # Handle cancellation + await interaction.response.edit_message(content="Operation cancelled.", view=None) + +class FileSearchCog(commands.Cog): + """Cog providing a file search and publish command.""" + + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.command(name="findfiles") + async def findfiles(self, ctx: commands.Context, entries: int, *, keywords: str): + """ + Search for files matching keywords and publish selected ones. + + Usage: !findfiles + """ + # Validate entries + if entries < 1 or entries > 10: + await ctx.send("❌ Entries must be between 1 and 10.") + return + # Parse keywords + keyword_list = keywords.split() + # Find matching files + files = file_search_functions.find_matches(entries, keyword_list) + if not files: + await ctx.send("🔍 No matching files found.") + return + # Send a message with a selection UI + view = FileSelectView(files) + file_list = "\n".join(f"{i+1}. {f}" for i, f in enumerate(files)) + await ctx.send( + f"🔍 Found files (select and click Accept or Cancel):\n{file_list}", + view=view + ) + +async def setup(bot: commands.Bot): + """Load the FileSearchCog.""" + await bot.add_cog(FileSearchCog(bot)) diff --git a/file_search_functions.py b/file_search_functions.py new file mode 100644 index 0000000..bf03954 --- /dev/null +++ b/file_search_functions.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import sys +import uuid +from pathlib import Path + +# CONFIGURATION +JSON_DB = '/var/log/share_scan.json' +SHARE_DIR = Path('/var/www/html/share') +BASE_URL = 'https://czernobog.pl/share' + +# Ensure share directory exists +SHARE_DIR.mkdir(parents=True, exist_ok=True) + +def load_db(): + with open(JSON_DB) as f: + return json.load(f)['entries'] + +ENTRIES = load_db() + +def relevancy(path, keywords): + score = 0 + low = path.lower() + for kw in keywords: + if kw.lower() in low: + score += low.count(kw.lower()) + return score + +def find_matches(count, keywords): + scored = [] + for e in ENTRIES: + score = relevancy(e['path'], keywords) + if score > 0: + scored.append((score, e['path'])) + scored.sort(reverse=True, key=lambda x: x[0]) + result = [p for _, p in scored] + return result[:count] + +def publish(paths): + urls = [] + for path in paths: + token = uuid.uuid4().hex + link = SHARE_DIR / token + try: + os.symlink(path, link) + except FileExistsError: + pass + urls.append(f"{BASE_URL}/{token}") + return urls From ee84d6ec32e19dc0831ac4c4535db4833f7b68ab Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Fri, 2 May 2025 19:17:27 +0200 Subject: [PATCH 12/22] more work needed --- file_search_functions.py | 51 ---------------------------------------- thin_client.py | 1 + 2 files changed, 1 insertion(+), 51 deletions(-) delete mode 100644 file_search_functions.py diff --git a/file_search_functions.py b/file_search_functions.py deleted file mode 100644 index bf03954..0000000 --- a/file_search_functions.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import json -import os -import sys -import uuid -from pathlib import Path - -# CONFIGURATION -JSON_DB = '/var/log/share_scan.json' -SHARE_DIR = Path('/var/www/html/share') -BASE_URL = 'https://czernobog.pl/share' - -# Ensure share directory exists -SHARE_DIR.mkdir(parents=True, exist_ok=True) - -def load_db(): - with open(JSON_DB) as f: - return json.load(f)['entries'] - -ENTRIES = load_db() - -def relevancy(path, keywords): - score = 0 - low = path.lower() - for kw in keywords: - if kw.lower() in low: - score += low.count(kw.lower()) - return score - -def find_matches(count, keywords): - scored = [] - for e in ENTRIES: - score = relevancy(e['path'], keywords) - if score > 0: - scored.append((score, e['path'])) - scored.sort(reverse=True, key=lambda x: x[0]) - result = [p for _, p in scored] - return result[:count] - -def publish(paths): - urls = [] - for path in paths: - token = uuid.uuid4().hex - link = SHARE_DIR / token - try: - os.symlink(path, link) - except FileExistsError: - pass - urls.append(f"{BASE_URL}/{token}") - return urls diff --git a/thin_client.py b/thin_client.py index efac6cb..38c4fa5 100755 --- a/thin_client.py +++ b/thin_client.py @@ -68,6 +68,7 @@ async def on_ready(): await client.load_extension("other_commands") await client.load_extension("voice_recognition_commands") + await client.load_extension("file_search_commands") logger.info("Sensors: online") logger.info(client.cogs) From 75932ec507778b3f3bd53fc3ce9658fa3488bb76 Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Fri, 2 May 2025 19:54:08 +0200 Subject: [PATCH 13/22] Search client --- ...rch_commqnds.py => file_search_commands.py | 0 file_search_functions.py | 31 +++++++++++++++++++ 2 files changed, 31 insertions(+) rename file_search_commqnds.py => file_search_commands.py (100%) create mode 100644 file_search_functions.py diff --git a/file_search_commqnds.py b/file_search_commands.py similarity index 100% rename from file_search_commqnds.py rename to file_search_commands.py diff --git a/file_search_functions.py b/file_search_functions.py new file mode 100644 index 0000000..b064447 --- /dev/null +++ b/file_search_functions.py @@ -0,0 +1,31 @@ +import requests + +# Base URL for Flask service +BASE_URL = 'http://192.168.1.15:5000' + + +def find_matches(entries, keywords): + """ + Call the get_share_list endpoint. + entries: int (1-10) + keywords: list of strings + Returns: list of file paths + """ + payload = {'entries': entries, 'keywords': keywords} + response = requests.post(f"{BASE_URL}/get_share_list", json=payload) + response.raise_for_status() + data = response.json() + return data.get('files', []) + + +def publish(file_paths): + """ + Call the get_share_links endpoint. + file_paths: list of file path strings + Returns: list of published URLs + """ + payload = {'file_paths': file_paths} + response = requests.post(f"{BASE_URL}/get_share_links", json=payload) + response.raise_for_status() + data = response.json() + return data.get('links', []) From 3ee7fcab59921419b72f7e56d14cb119ce18f40d Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Fri, 2 May 2025 19:46:42 +0200 Subject: [PATCH 14/22] serverside --- conjurer_musician/conjurer_musician.py | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index 16e18c4..6775cf6 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -332,6 +332,36 @@ def remove_characters(string, character): return string.replace(character, "") +@app.route('/get_share_list', methods=['POST']) +def get_share_list(): + data = request.get_json() + entries = data.get('entries') + keywords = data.get('keywords') + # Validate entries + if not isinstance(entries, int) or not (1 <= entries <= 10): + return jsonify({'error': '"entries" must be an integer between 1 and 10.'}), 400 + # Validate keywords list + if not isinstance(keywords, list) or not all(isinstance(k, str) for k in keywords): + return jsonify({'error': '"keywords" must be a list of strings.'}), 400 + # Call external search + files = media_search_functions.find_matches(entries, keywords) + # Return each file path as a JSON list + return jsonify({'files': files}), 200 + +@app.route('/get_share_links', methods=['POST']) +def get_share_links(): + data = request.get_json() + file_paths = data.get('file_paths') + # Validate file_paths list + if not isinstance(file_paths, list) or not all(isinstance(p, str) for p in file_paths): + return jsonify({'error': '"file_paths" must be a list of strings.'}), 400 + # Call external publish + links = media_search_functions.publish(file_paths) + # Return list of published links + return jsonify({'links': links}), 200 + + + @app.route("/stream", methods=["GET"]) def stream_music(): """ From 78c958e8f5b89bcb36e20d10bd8f85d64bd023d0 Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Fri, 2 May 2025 19:55:28 +0200 Subject: [PATCH 15/22] added server side search --- conjurer_musician/conjurer_musician.py | 2 + conjurer_musician/media_search_functions.py | 52 +++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 conjurer_musician/media_search_functions.py diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index 6775cf6..12bcf5d 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -29,6 +29,8 @@ from flask import ( send_from_directory, ) from waitress import serve +import media_search_functions + MAIN_BOT_ADDRESS = "http://192.168.1.191:5000" MUSIC_TRACKER = "/prepped_tracks" diff --git a/conjurer_musician/media_search_functions.py b/conjurer_musician/media_search_functions.py new file mode 100644 index 0000000..87a2712 --- /dev/null +++ b/conjurer_musician/media_search_functions.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import sys +import uuid +from pathlib import Path + +# CONFIGURATION +JSON_DB = '/var/log/share_scan.json' +SHARE_DIR = Path('/var/www/html/share') +BASE_URL = 'https://czernobog.pl/share' + +# Ensure share directory exists +SHARE_DIR.mkdir(parents=True, exist_ok=True) + +def load_db(): + with open(JSON_DB) as f: + return json.load(f)['entries'] + +ENTRIES = load_db() + +def relevancy(path, keywords): + score = 0 + low = path.lower() + for kw in keywords: + if kw.lower() in low: + score += low.count(kw.lower()) + return score + +def find_matches(count, keywords): + scored = [] + for e in ENTRIES: + score = relevancy(e['path'], keywords) + if score > 0: + scored.append((score, e['path'])) + scored.sort(reverse=True, key=lambda x: x[0]) + result = [p for _, p in scored] + return result[:count] + +def publish(paths): + urls = [] + for path in paths: + token = uuid.uuid4().hex + link = SHARE_DIR / token + try: + os.symlink(path, link) + except FileExistsError: + pass + urls.append(f"{BASE_URL}/{token}") + return urls + From d0c972c69bd88765c1035d53276a63efd0d6b4d8 Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Fri, 2 May 2025 20:09:11 +0200 Subject: [PATCH 16/22] M --- conjurer_musician/revoke_shares.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conjurer_musician/revoke_shares.py b/conjurer_musician/revoke_shares.py index c68822b..a3f19f3 100755 --- a/conjurer_musician/revoke_shares.py +++ b/conjurer_musician/revoke_shares.py @@ -66,7 +66,7 @@ def record_downloads(lines, metadata, args): def revoke_old(metadata, args): now = time.time() - to_remove = [tok for tok, ts in metadata.items() if now - ts >= 120] + to_remove = [tok for tok, ts in metadata.items() if now - ts >= 3600] for tok in to_remove: link = SHARE_DIR / tok try: From a79aa4e9a75a9e047cdb71a0cba38f4588507125 Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Fri, 2 May 2025 20:10:10 +0200 Subject: [PATCH 17/22] fix --- file_search_commands.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/file_search_commands.py b/file_search_commands.py index 0f09bc7..7e76b0c 100644 --- a/file_search_commands.py +++ b/file_search_commands.py @@ -6,7 +6,6 @@ import file_search_functions class FileSelectView(discord.ui.View): def __init__(self, files): super().__init__(timeout=60) - # Create select options for each file options = [discord.SelectOption(label=os.path.basename(f)[:100], description=f, value=f) for f in files] self.select = discord.ui.Select( placeholder="Select files...", @@ -24,7 +23,7 @@ class FileSelectView(discord.ui.View): await interaction.response.defer() @discord.ui.button(label="Accept", style=discord.ButtonStyle.green) - async def accept_callback(self, button: discord.ui.Button, interaction: discord.Interaction): + async def accept_callback(self, interaction: discord.Interaction, button: discord.ui.Button): # Handle acceptance if not self.selected: await interaction.response.send_message("No files selected.", ephemeral=True) @@ -35,7 +34,7 @@ class FileSelectView(discord.ui.View): await interaction.response.edit_message(content="Published Links:\n" + "\n".join(links), view=None) @discord.ui.button(label="Cancel", style=discord.ButtonStyle.red) - async def cancel_callback(self, button: discord.ui.Button, interaction: discord.Interaction): + async def cancel_callback(self, interaction: discord.Interaction, button: discord.ui.Button): # Handle cancellation await interaction.response.edit_message(content="Operation cancelled.", view=None) @@ -52,18 +51,14 @@ class FileSearchCog(commands.Cog): Usage: !findfiles """ - # Validate entries if entries < 1 or entries > 10: await ctx.send("❌ Entries must be between 1 and 10.") return - # Parse keywords keyword_list = keywords.split() - # Find matching files files = file_search_functions.find_matches(entries, keyword_list) if not files: await ctx.send("🔍 No matching files found.") return - # Send a message with a selection UI view = FileSelectView(files) file_list = "\n".join(f"{i+1}. {f}" for i, f in enumerate(files)) await ctx.send( @@ -72,5 +67,4 @@ class FileSearchCog(commands.Cog): ) async def setup(bot: commands.Bot): - """Load the FileSearchCog.""" await bot.add_cog(FileSearchCog(bot)) From c7af223155b68c56754461a55c98f54671fca4b4 Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Fri, 2 May 2025 20:12:33 +0200 Subject: [PATCH 18/22] fix deploy --- deploy.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/deploy.sh b/deploy.sh index ff0e8d9..0651f6a 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,6 +1,6 @@ #!/bin/bash -total_commands=24 +total_commands=26 current_command=0 function print_progress { @@ -79,8 +79,16 @@ print_progress "cp ./conjurer/radio_commands.py ./Conjurer/" cp ./conjurer/voice_recognition_commands.py ./Conjurer/ print_progress "cp ./conjurer/voice_recognition_commands.py ./Conjurer/" +cp ./conjurer/file_search_functions.py ./Conjurer/ + +print_progress "cp ./conjurer/file_search_functions.py ./Conjurer/" + +cp ./conjurer/file_search_commands.py ./Conjurer +print_progress "cp ./conjurer/file_search_functions.py ./Conjurer/" + + cp ./conjurer/thin_client.py ./Conjurer/bot.py print_progress "cp ./conjurer/thin_client.py ./Conjurer/bot.py" sudo systemctl restart conjurer.service -print_progress "sudo systemctl restart conjurer.service" \ No newline at end of file +print_progress "sudo systemctl restart conjurer.service" From 7edd60c10a268df7fed1e7fc0ef38ef1451aa36c Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Fri, 2 May 2025 20:20:15 +0200 Subject: [PATCH 19/22] zmiana nazwy --- file_search_commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/file_search_commands.py b/file_search_commands.py index 7e76b0c..1e20850 100644 --- a/file_search_commands.py +++ b/file_search_commands.py @@ -44,7 +44,8 @@ class FileSearchCog(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - @commands.command(name="findfiles") + @commands.has_any_role("Jarl", "Thane") + @commands.command(name="tajna_biblioteka_inkwizycji") async def findfiles(self, ctx: commands.Context, entries: int, *, keywords: str): """ Search for files matching keywords and publish selected ones. From 074452e6a9aa4a404acab2b1883c14e15e650b13 Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Sat, 3 May 2025 16:41:14 +0200 Subject: [PATCH 20/22] Params of radio --- conjurer_musician/script.params | 195 ++++++++++++++++++++++++++++++++ deploy_music.sh | 3 +- 2 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 conjurer_musician/script.params diff --git a/conjurer_musician/script.params b/conjurer_musician/script.params new file mode 100644 index 0000000..f8837f0 --- /dev/null +++ b/conjurer_musician/script.params @@ -0,0 +1,195 @@ +{ + "string": [], + "bool": [ [ "Skip track", false ], [ "Going Live!", false ] ], + "int": [], + "float": [ + [ + "mic_volume", + 0.0 + ], + [ + "compress_gain6", + 0.1 + ], + [ + "compress_ratio6", + 7.9 + ], + [ + "compress_threshold6", + -15.8 + ], + [ + "compress_release6", + 30.0 + ], + [ + "compress_attack6", + 30.0 + ], + [ + "compress_frequency6", + 18610.0 + ], + [ + "compress_gain5", + 5.4 + ], + [ + "compress_ratio5", + 4.8 + ], + [ + "compress_threshold5", + -13.6 + ], + [ + "compress_release5", + 170.0 + ], + [ + "compress_attack5", + 160.0 + ], + [ + "compress_frequency5", + 13950.0 + ], + [ + "compress_gain4", + 6.5 + ], + [ + "compress_ratio4", + 4.0 + ], + [ + "compress_threshold4", + -12.2 + ], + [ + "compress_release4", + 180.0 + ], + [ + "compress_attack4", + 200.0 + ], + [ + "compress_frequency4", + 3890.0 + ], + [ + "compress_gain3", + 8.2 + ], + [ + "compress_ratio3", + 3.5 + ], + [ + "compress_threshold3", + -11.7 + ], + [ + "compress_release3", + 180.0 + ], + [ + "compress_attack3", + 250.0 + ], + [ + "compress_frequency3", + 2270.0 + ], + [ + "compress_gain2", + 7.4 + ], + [ + "compress_ratio2", + 3.9 + ], + [ + "compress_threshold2", + -11.5 + ], + [ + "compress_release2", + 190.0 + ], + [ + "compress_attack2", + 250.0 + ], + [ + "compress_frequency2", + 1920.0 + ], + [ + "compress_gain1", + 11.1 + ], + [ + "compress_ratio1", + 3.4 + ], + [ + "compress_threshold1", + -12.7 + ], + [ + "compress_release1", + 170.0 + ], + [ + "compress_attack1", + 220.0 + ], + [ + "compress_frequency1", + 650.0 + ], + [ + "compress_gain0", + 16.0 + ], + [ + "compress_ratio0", + 3.2 + ], + [ + "compress_threshold0", + -15.3 + ], + [ + "compress_release0", + 200.0 + ], + [ + "compress_attack0", + 90.0 + ], + [ + "compress_frequency0", + 110.0 + ], + [ + "compress_wet", + 1.0 + ], + [ + "main_volume", + 1.8 + ], + [ + "g", + 9.5 + ], + [ + "f", + 64.2 + ] + ] +} \ No newline at end of file diff --git a/deploy_music.sh b/deploy_music.sh index 7914d31..6253a7b 100755 --- a/deploy_music.sh +++ b/deploy_music.sh @@ -2,5 +2,6 @@ cd ./conjurer || exit git pull cd .. +cp ./conjurer/conjurer_musician/media_search_functions.py ./Conjurer cp ./conjurer/conjurer_musician/conjurer_musician.py ./Conjurer -sudo systemctl restart conjurer_musician.service \ No newline at end of file +sudo systemctl restart conjurer_musician.service From b778a1c3f9cf9e111c740e8fead595279cd73ec4 Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Sun, 4 May 2025 16:15:45 +0200 Subject: [PATCH 21/22] Watcher --- watch_script_params.py | 63 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100755 watch_script_params.py diff --git a/watch_script_params.py b/watch_script_params.py new file mode 100755 index 0000000..5d2e20e --- /dev/null +++ b/watch_script_params.py @@ -0,0 +1,63 @@ +import os +import time +import shutil +from datetime import datetime, date +import hashlib +import subprocess + +# File paths +SOURCE_FILE = "/home/pi/Conjurer/script.params" +BACKUP_DIR = "/home/pi/Conjurer" +GIT_REPO_DIR = "/home/pi/conjurer/conjurer_musician" +LAST_HASH_FILE = "/home/pi/Conjurer/.last_hash" +LAST_GIT_COMMIT_FILE = "/home/pi/Conjurer/.last_git_commit" + +def compute_file_hash(filepath): + with open(filepath, 'rb') as f: + return hashlib.sha256(f.read()).hexdigest() + +def backup_file(): + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + backup_path = os.path.join(BACKUP_DIR, f"{timestamp}_script.params") + shutil.copy2(SOURCE_FILE, backup_path) + +def commit_to_git(): + try: + subprocess.run(["cp", SOURCE_FILE, os.path.join(GIT_REPO_DIR, "script.params")], check=True) + subprocess.run(["git", "-C", GIT_REPO_DIR, "add", "script.params"], check=True) + subprocess.run(["git", "-C", GIT_REPO_DIR, "commit", "-m", f"Daily update: {datetime.now()}"], check=True) + subprocess.run(["git", "-C", GIT_REPO_DIR, "push"], check=True) + except subprocess.CalledProcessError as e: + print(f"Git operation failed: {e}") + +def main(): + if not os.path.exists(SOURCE_FILE): + return + + current_hash = compute_file_hash(SOURCE_FILE) + + # Detect change + last_hash = None + if os.path.exists(LAST_HASH_FILE): + with open(LAST_HASH_FILE, 'r') as f: + last_hash = f.read().strip() + + if current_hash != last_hash: + backup_file() + with open(LAST_HASH_FILE, 'w') as f: + f.write(current_hash) + + # Daily git commit + today = str(date.today()) + last_commit_date = "" + if os.path.exists(LAST_GIT_COMMIT_FILE): + with open(LAST_GIT_COMMIT_FILE, 'r') as f: + last_commit_date = f.read().strip() + + if today != last_commit_date: + commit_to_git() + with open(LAST_GIT_COMMIT_FILE, 'w') as f: + f.write(today) + +if __name__ == "__main__": + main() From 1f5b67813976a9e0cde1c664cb874195b2bae5f7 Mon Sep 17 00:00:00 2001 From: Polish Hammer Date: Sun, 4 May 2025 21:15:28 +0200 Subject: [PATCH 22/22] small fixes --- conjurer_musician/conjurer_musician.py | 6 +++--- conjurer_musician/radio_conjurer.liq | 27 ++++++++++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/conjurer_musician/conjurer_musician.py b/conjurer_musician/conjurer_musician.py index 12bcf5d..548e90e 100644 --- a/conjurer_musician/conjurer_musician.py +++ b/conjurer_musician/conjurer_musician.py @@ -50,10 +50,10 @@ if platform in ("linux", "linux2"): else: LOGFILE = "/home/pi/Conjurer/discord_mus_service.log" NETRC_FILE = "/home/pi/.netrc" - LOGSTORE = "/home/pi/MediaShare/logs/" + LOGSTORE = "/home/pi/MediaFolder/logs/" ENCODING = "utf-8" - MUSIC_FOLDER = "/home/pi/MediaShare/mp3/" - PRIORITY_FOLDER = "/home/pi/MediaShare/mp3/Magiczne i chuj/" + MUSIC_FOLDER = "/home/pi/MediaFolder/mp3/" + PRIORITY_FOLDER = "/home/pi/MediaFoldermp3/Magiczne i chuj/" RADIOLOG_PATH = "/home/pi/Conjurer/radio_log.log" PERSISTENCE_PATH = "/home/pi/Conjurer/persistence.log" diff --git a/conjurer_musician/radio_conjurer.liq b/conjurer_musician/radio_conjurer.liq index d536607..b28c947 100644 --- a/conjurer_musician/radio_conjurer.liq +++ b/conjurer_musician/radio_conjurer.liq @@ -43,6 +43,22 @@ def queue_processing() end end +# Configure output formats and destinations +def now_playing(m) + # raw lookup ("" if missing) + raw_title = m["title"] + raw_artist = m["artist"] + + # default if empty + title = if raw_title == "" then "Unknown" else raw_title end + artist = if raw_artist == "" then "Unknown" else raw_artist end + log.critical("Now playing #{artist} - #{title}") + # write it out + system("echo '#{artist} - #{title}' > /tmp/now_playing.txt") + m +end + + # Randomly select a playlist with weights s4 = random(id="randomizer", weights=[2, 3, 5], [s1, s2, s3]) @@ -52,7 +68,8 @@ jingles = (playlist(reload_mode="watch", "/home/pi/Conjurer/jingles.playlist")) # Create the main stream with random playlist and jingles s = rotate(id="randomizer", weights=[10, 1], [s4, jingles]) -s = crossfade(fade_out=2., fade_in=2., duration=4., fallback(id="switcher", track_sensitive=false, [requests_queue, s])) +s = crossfade(fade_out=2., fade_in=2., duration=4., fallback(id="switcher", track_sensitive=true, [requests_queue, s])) +s = metadata.map(now_playing, s) # Set up an interactive harbor for controlling the stream interactive.harbor(port = 9999) @@ -87,7 +104,7 @@ s = blank.skip(max_blank=10., s) live_enabled = interactive.bool("Going Live!", true) s = add([mic,s]) -s=switch(track_sensitive=false, +s=switch(track_sensitive=true, [(live_enabled, mic), ({true}, s)]) @@ -102,9 +119,8 @@ set("log.level", loglevel) # Enable logging to file set("log.file", log_to_file) set("log.file.path", logpath) - # Set up emergency fallback track -emergency = single("/home/pi/RetroPie/mp3/Youtube/Dr. Peacock - Trip to Ireland [GvrvQTUbUcA].mp3") +emergency = single("/home/pi/MediaFolder/mp3/Youtube/Dr. Peacock - Trip to Ireland [GvrvQTUbUcA].mp3") radio = fallback(id="switcher2", track_sensitive=false, [s, emergency]) # Set up an interactive control for skipping tracks @@ -138,8 +154,7 @@ interactive.persistent("script.params") # Configure output formats and destinations - -output.icecast(%mp3, host="retropie", port=8000, password=credentials.password, icy_metadata="true", mount="mp3-stream", radio) +output.icecast(%mp3, host="radio", port=8000, password=credentials.password, icy_metadata="true", mount="mp3-stream", radio) output.pulseaudio(radio) #output.file.hls("/tmp/hls", [("mp3-low", %mp3(bitrate=96)), ("mp3-hi", %mp3(bitrate=160))], radio) # Uncomment the following lines to enable additional output formats