main
agropunx 11 months ago
parent ed95179bff
commit 38f55b5b56

@ -1,8 +1,9 @@
# Celaigia
ascolti lo stesso pezzo a ripetizione su youtube? provi antipatia per gesu? usa celaigia!
celaigia è un player musicale scritto con python (pygame & dearpygui mostly).
Oltre alle classiche funzionalità audio player, con celaigia puoi cercare e scaricare le tue canzoncine preferite direttamente da youtube (yt-dlp).
Welcome to celaigia
an app to gently query youtube music, and possibly download only if you don't have already locally
# TODO
- explore the db
- nicer ui
- contenerize
- mobile

@ -0,0 +1,12 @@
import importlib
import config
importlib.reload(config)
import utils
importlib.reload(utils)
import database
importlib.reload(database)
import ui
importlib.reload(ui)
import app
importlib.reload(app)

@ -0,0 +1,9 @@
from config import music_path
from ui import UIEngine
from database import Database
if __name__ == "__main__":
# Add your main code here
db = Database(base_path=music_path)
ui = UIEngine(db)
ui.run()

@ -1,301 +0,0 @@
import pygame
import threading
import time
import random
import os
import atexit
import ntpath
import json
import webbrowser
import subprocess
import dearpygui.dearpygui as dpg
from mutagen.mp3 import MP3
from tkinter import Tk,filedialog, simpledialog, Button, OptionMenu, N, S , E, W, Label, StringVar
import pytube
dpg.create_context()
dpg.create_viewport(title="celaigia, stai senza pensieri",large_icon="logo.ico",small_icon="logo.ico")
pygame.mixer.init()
#https://www.redhat.com/sysadmin/write-GUI-applications-python
global state
state=None
_SONG_FILE = "data/songs.json"
_NUM_YT_SEARCH_RESULTS = 5
_DEFAULT_DOWNLOAD_PATH = 'data/music'
_DEFAULT_MUSIC_VOLUME = 0.5
pygame.mixer.music.set_volume(_DEFAULT_MUSIC_VOLUME)
def bash(cmd):
subprocess.call(['/bin/bash', '-c', cmd])
def string_sanitizer(s: str)->str:
s = ''.join(filter(lambda x: x in '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ', s))
return s.replace(' ','_')
def ismusic(filename: str)->bool:
return filename.split('.')[-1] in ['mp3', 'ogg', 'flac', 'wav']
def update_volume(sender, app_data):
pygame.mixer.music.set_volume(app_data / 100.0)
def load_database():
songs = json.load(open(_SONG_FILE, "r+"))["songs"]
for filename in songs:
dpg.add_button(
label=f"{ntpath.basename(filename)}",
callback=play,
width=-1,
height=25,
user_data=filename.replace("\\", "/"),
parent="list"
)
dpg.add_spacer(height=2, parent="list")
def update_database(filename: str):
data = json.load(open(_SONG_FILE, "r+"))
if filename not in data["songs"]:
data["songs"] += [filename]
dpg.add_button(
label=f"{ntpath.basename(filename)}",
callback=play,
width=-1,
height=25,
user_data=filename.replace("\\", "/"),
parent="list"
)
dpg.add_spacer(height=2, parent="list")
json.dump(data, open(_SONG_FILE, "r+"), indent=4)
def update_slider():
global state
while pygame.mixer.music.get_busy():
dpg.configure_item(item="pos",default_value=pygame.mixer.music.get_pos()/1000)
time.sleep(0.7)
state=None
dpg.configure_item("cstate",default_value=f"State: None")
dpg.configure_item("csong",default_value="Now Playing : ")
dpg.configure_item("play",label="Play")
dpg.configure_item(item="pos",max_value=100)
dpg.configure_item(item="pos",default_value=0)
def play(sender, app_data, user_data):
global state
if user_data:
pygame.mixer.music.load(user_data)
audio = MP3(user_data)
dpg.configure_item(item="pos",max_value=audio.info.length)
pygame.mixer.music.play()
thread=threading.Thread(target=update_slider,daemon=False).start()
if pygame.mixer.music.get_busy():
dpg.configure_item("play",label="Pause")
state="playing"
dpg.configure_item("cstate",default_value=f"State: Playing")
dpg.configure_item("csong",default_value=f"Now Playing : {ntpath.basename(user_data)}")
def play_pause():
global state
if state=="playing":
state="paused"
pygame.mixer.music.pause()
dpg.configure_item("play",label="Play")
dpg.configure_item("cstate",default_value=f"State: Paused")
elif state=="paused":
state="playing"
pygame.mixer.music.unpause()
dpg.configure_item("play",label="Pause")
dpg.configure_item("cstate",default_value=f"State: Playing")
else:
song = json.load(open(_SONG_FILE, "r"))["songs"]
if song:
song=random.choice(song)
pygame.mixer.music.load(song)
pygame.mixer.music.play()
thread=threading.Thread(target=update_slider,daemon=False).start()
dpg.configure_item("play",label="Pause")
if pygame.mixer.music.get_busy():
audio = MP3(song)
dpg.configure_item(item="pos",max_value=audio.info.length)
state="playing"
dpg.configure_item("csong",default_value=f"Now Playing : {ntpath.basename(song)}")
dpg.configure_item("cstate",default_value=f"State: Playing")
def stop():
global state
pygame.mixer.music.stop()
state=None
def add_files():
data=json.load(open(_SONG_FILE,"r"))
root=Tk()
root.withdraw()
filename=filedialog.askopenfilename(filetypes=[("Music Files", ("*.mp3","*.wav","*.ogg"))])
root.quit()
if filename.endswith(".mp3" or ".wav" or ".ogg"):
if filename not in data["songs"]:
update_database(filename)
#dpg.add_button(label=f"{ntpath.basename(filename)}",callback=play,width=-1,height=25,,parent="list")
#dpg.add_spacer(height=2,parent="list")
def add_folder():
data=json.load(open(_SONG_FILE,"r"))
root=Tk()
root.withdraw()
folder=filedialog.askdirectory()
root.quit()
for filename in os.listdir(folder):
if filename.endswith(".mp3" or ".wav" or ".ogg"):
if filename not in data["songs"]:
update_database(os.path.join(folder,filename).replace("\\","/"))
#dpg.add_button(label=f"{ntpath.basename(filename)}",callback=play,width=-1,height=25,user_data=os.path.join(folder,filename).replace("\\","/"),parent="list")
#dpg.add_spacer(height=2,parent="list")
def search(sender, app_data, user_data):
songs = json.load(open(_SONG_FILE, "r"))["songs"]
dpg.delete_item("list", children_only=True)
for index, song in enumerate(songs):
if app_data in song.lower():
dpg.add_button(label=f"{ntpath.basename(song)}", callback=play,width=-1, height=25, user_data=song, parent="list")
dpg.add_spacer(height=2,parent="list")
def add_download_choices(sender, app_data, user_data)->None:
yt_urls = {string_sanitizer(r.title): r.watch_url for r in pytube.Search(app_data).results[:_NUM_YT_SEARCH_RESULTS]}
for url_key in yt_urls:
dpg.add_button(label=url_key, tag=url_key, callback= download ,width=-1, height=25, user_data = (url_key,yt_urls), parent="sidebar")
dpg.add_spacer(height=2,parent="sidebar")
def download(sender, app_data, user_data):
url_key, yt_urls = user_data
url = yt_urls[url_key]
for tmp_url_key in yt_urls:
dpg.delete_item(tmp_url_key)
video_file = f'{_DEFAULT_DOWNLOAD_PATH}/{url_key}.mp4'
audio_file = f"{video_file[:-4]}.mp3"
stream = pytube.YouTube(url).streams.filter(only_audio=True).first()
stream.download(filename=video_file, skip_existing=True)
bash(f"ffmpeg -i {video_file} -vn -acodec mp3 -y {audio_file}")
bash(f"rm {video_file}")
update_database(audio_file)
def removeall():
songs = json.load(open(_SONG_FILE, "r"))
songs["songs"].clear()
json.dump(songs,open(_SONG_FILE, "w"),indent=4)
dpg.delete_item("list", children_only=True)
load_database()
with dpg.theme(tag="base"):
with dpg.theme_component():
dpg.add_theme_color(dpg.mvThemeCol_Button, (130, 142, 250))
dpg.add_theme_color(dpg.mvThemeCol_ButtonActive, (137, 142, 255, 95))
dpg.add_theme_color(dpg.mvThemeCol_ButtonHovered, (137, 142, 255))
dpg.add_theme_style(dpg.mvStyleVar_FrameRounding, 3)
dpg.add_theme_style(dpg.mvStyleVar_ChildRounding, 4)
dpg.add_theme_style(dpg.mvStyleVar_FramePadding, 4, 4)
dpg.add_theme_style(dpg.mvStyleVar_WindowRounding, 4, 4)
dpg.add_theme_style(dpg.mvStyleVar_WindowTitleAlign, 0.50, 0.50)
dpg.add_theme_style(dpg.mvStyleVar_WindowBorderSize,0)
dpg.add_theme_style(dpg.mvStyleVar_WindowPadding,10,14)
dpg.add_theme_color(dpg.mvThemeCol_ChildBg, (25, 25, 25))
dpg.add_theme_color(dpg.mvThemeCol_Border, (0,0,0,0))
dpg.add_theme_color(dpg.mvThemeCol_ScrollbarBg, (0,0,0,0))
dpg.add_theme_color(dpg.mvThemeCol_TitleBgActive, (130, 142, 250))
dpg.add_theme_color(dpg.mvThemeCol_CheckMark, (221, 166, 185))
dpg.add_theme_color(dpg.mvThemeCol_FrameBgHovered, (172, 174, 197))
with dpg.theme(tag="slider_thin"):
with dpg.theme_component():
dpg.add_theme_color(dpg.mvThemeCol_FrameBgActive, (130, 142, 250,99))
dpg.add_theme_color(dpg.mvThemeCol_FrameBgHovered, (130, 142, 250,99))
dpg.add_theme_color(dpg.mvThemeCol_SliderGrabActive, (255, 255, 255))
dpg.add_theme_color(dpg.mvThemeCol_SliderGrab, (255, 255, 255))
dpg.add_theme_color(dpg.mvThemeCol_FrameBg, (130, 142, 250,99))
dpg.add_theme_style(dpg.mvStyleVar_GrabRounding, 3)
dpg.add_theme_style(dpg.mvStyleVar_GrabMinSize, 30)
with dpg.theme(tag="slider"):
with dpg.theme_component():
dpg.add_theme_color(dpg.mvThemeCol_FrameBgActive, (130, 142, 250,99))
dpg.add_theme_color(dpg.mvThemeCol_FrameBgHovered, (130, 142, 250,99))
dpg.add_theme_color(dpg.mvThemeCol_SliderGrabActive, (255, 255, 255))
dpg.add_theme_color(dpg.mvThemeCol_SliderGrab, (255, 255, 255))
dpg.add_theme_color(dpg.mvThemeCol_FrameBg, (130, 142, 250,99))
dpg.add_theme_style(dpg.mvStyleVar_GrabRounding, 3)
dpg.add_theme_style(dpg.mvStyleVar_GrabMinSize, 30)
with dpg.theme(tag="songs"):
with dpg.theme_component():
dpg.add_theme_style(dpg.mvStyleVar_FrameRounding, 2)
dpg.add_theme_color(dpg.mvThemeCol_Button, (89, 89, 144,40))
dpg.add_theme_color(dpg.mvThemeCol_ButtonHovered, (0,0,0,0))
with dpg.font_registry():
monobold = dpg.add_font("fonts/MonoLisa-Bold.ttf", 12)
head = dpg.add_font("fonts/MonoLisa-Bold.ttf", 15)
with dpg.window(tag="main",label="window title"):
with dpg.child_window(autosize_x=True,height=45,no_scrollbar=True):
dpg.add_text(f"Now Playing : ",tag="csong")
dpg.add_spacer(height=2)
with dpg.group(horizontal=True):
with dpg.child_window(width=250, tag="sidebar"):
dpg.add_text("Celaigia",color=(137, 142, 255))
dpg.add_text("phuturemachine")
dpg.add_spacer(height=2)
dpg.add_button(label="Support",width=-1,height=23,callback=lambda:webbrowser.open(url="gitgitigitigiti"))
dpg.add_spacer(height=5)
dpg.add_separator()
dpg.add_spacer(height=5)
dpg.add_button(label="Add File",width=-1,height=28,callback=add_files)
dpg.add_button(label="Add Folder",width=-1,height=28,callback=add_folder)
dpg.add_button(label="Remove All Songs",width=-1,height=28,callback=removeall)
dpg.add_spacer(height=5)
dpg.add_separator()
dpg.add_spacer(height=5)
dpg.add_text(f"State: {state}",tag="cstate")
dpg.add_spacer(height=5)
dpg.add_separator()
dpg.add_input_text(hint="search youtube",width=-1,callback=add_download_choices, on_enter=True)
dpg.add_spacer(height=5)
with dpg.child_window(autosize_x=True,border=False):
with dpg.child_window(autosize_x=True,height=50,no_scrollbar=True):
with dpg.group(horizontal=True):
dpg.add_button(label="Play",width=65,height=30,tag="play",callback=play_pause)
dpg.add_button(label="Stop",callback=stop,width=65,height=30)
dpg.add_slider_float(tag="volume", width=120,height=15,pos=(160,19),format="%.0f%.0%",default_value=_DEFAULT_MUSIC_VOLUME * 100,callback=update_volume)
dpg.add_slider_float(tag="pos",width=-1,pos=(295,19),format="")
with dpg.child_window(autosize_x=True,delay_search=True):
with dpg.group(horizontal=True,tag="query"):
dpg.add_input_text(hint="search for a song locally",width=-1,callback=search)
dpg.add_spacer(height=5)
with dpg.child_window(autosize_x=True,delay_search=True,tag="list"):
load_database()
dpg.bind_item_theme("volume","slider_thin")
dpg.bind_item_theme("pos","slider")
dpg.bind_item_theme("list","songs")
dpg.bind_theme("base")
dpg.bind_font(monobold)
def safe_exit():
pygame.mixer.music.stop()
pygame.quit()
atexit.register(safe_exit)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.set_primary_window("main",True)
dpg.maximize_viewport()
dpg.start_dearpygui()
dpg.destroy_context()

@ -0,0 +1,11 @@
music_path = "./music"
yt_dlp_opts = {
'format': 'bestaudio/best',
'default_search': 'ytsearch5',
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredquality': '192',
}],
}

@ -1,6 +0,0 @@
{
"songs": [
"data/music/Mac_Miller__Self_Care.mp3",
"data/music/COLLE_DER_FOMENTO__Il_cielo_su_Roma.mp3"
]
}

@ -0,0 +1,37 @@
# Updated database.py
import json
import datetime
class Database:
def __init__(self, base_path):
self.base_path = base_path
self.file_path = f'{base_path}/index.json'
try:
with open(self.file_path, 'r') as file:
self.data = json.load(file)
except FileNotFoundError:
self.data = []
def add_entry(self, entry):
entry['timestamp'] = datetime.datetime.now().strftime('%d/%m/%Y')
self.data.append(entry)
with open(self.file_path, 'w') as file:
json.dump(self.data, file, indent=4)
def downloadable(self, url, title):
idx = url.split('https://www.youtube.com/watch?v=')[-1]
for entry in self.data:
if entry['id'] == idx or entry['title'] == title:
return False
return True
def get_all_entries(self):
return self.data
def update_entry(self, entry_id, new_data):
for entry in self.data:
if entry['id'] == entry_id:
entry.update(new_data)
with open(self.file_path, 'w') as file:
json.dump(self.data, file, indent=4)
return

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

@ -0,0 +1,102 @@
[
{
"id": "debIyWS6Byc",
"title": "Actin Crazy",
"artist": "action bronson",
"filename": "Action Bronson - Actin Crazy (Official Music Video)",
"tags": [
"hip hop",
"rap",
"flow"
],
"timestamp": "31/12/2023"
},
{
"id": "xEhwlJQMVO8",
"title": "blue chips full album",
"artist": "action bronson",
"filename": "Action Bronson - Blue Chips (Full Album)",
"tags": [
"rap",
"hip hop",
""
],
"timestamp": "31/12/2023"
},
{
"id": "C3LXkdpHNOQ",
"title": "Waiting Room",
"artist": "Fugazi",
"filename": "Waiting Room",
"tags": [
"punk",
"hardcore",
"diy",
"fugazi"
],
"timestamp": "31/12/2023"
},
{
"id": "zG2TgCuMcjM",
"title": "Death in Midsummer",
"artist": "Deerhunter",
"filename": "Deerhunter - Death in Midsummer (Official Video)",
"tags": [
"rock"
],
"timestamp": "31/12/2023"
},
{
"id": "ZycbeMV6_4s",
"title": "Non Ci Sto",
"artist": "colle der fomento",
"filename": "Non Ci Sto - Colle Der Fomento",
"tags": [
"hip hop",
"italiano",
"rap"
],
"timestamp": "31/12/2023"
},
{
"id": "ukgraQ-xkp4",
"title": "The Court Of The Crimson King",
"artist": "King Crimson",
"filename": "King Crimson - The Court Of The Crimson King",
"tags": [],
"timestamp": "31/12/2023"
},
{
"id": "FhKJgqxNDD8",
"title": "Starless",
"artist": "King Crimson",
"filename": "King Crimson - Starless",
"tags": [],
"timestamp": "31/12/2023"
},
{
"id": "PBwAxmrE194",
"title": "C.R.E.A.M.",
"artist": "wu tang clan",
"filename": "Wu-Tang Clan - C.R.E.A.M. (Official HD Video)",
"tags": [
"hip hop",
"90s",
"rap"
],
"timestamp": "31/12/2023"
},
{
"id": "00uHDPrL8cQ",
"title": "Piombo e Fango",
"artist": "colle der fomento",
"filename": "Danno Piombo e Fango",
"tags": [
"hip hop",
"rap",
"danno",
"italiano"
],
"timestamp": "31/12/2023"
}
]

@ -1,6 +1 @@
dearpygui
mutagen
pygame
pytube
tk
yt-dlp

137
ui.py

@ -0,0 +1,137 @@
import tkinter as tk
from tkinter import ttk
import tkinter.simpledialog as sd
import time
import os
from utils import search_youtube, download_audio
class UIEngine:
def __init__(self, db):
self.db = db
self.root = tk.Tk()
self.root.title("celaigia")
self.root.geometry("400x300") # Set a fixed size
# Create a PanedWindow to split the UI vertically
self.paned_window = tk.PanedWindow(self.root, orient=tk.VERTICAL)
self.paned_window.pack(expand=True, fill='both')
# Create the top part of the UI
self.top_frame = ttk.Frame(self.paned_window)
self.paned_window.add(self.top_frame)
self.status_label = tk.Label(self.top_frame, text="Ready")
self.status_label.pack()
self.search_frame = ttk.Frame(self.top_frame)
self.search_frame.pack()
self.download_frame = ttk.Frame(self.top_frame)
self.download_frame.pack()
self.search_status_label = tk.Label(self.search_frame, text="Enter search query:")
self.search_status_label.grid(row=0, column=0)
self.search_entry = tk.Entry(self.search_frame)
self.search_entry.grid(row=0, column=1)
self.search_button = tk.Button(self.search_frame, text="Search", command=lambda: self.search_and_display())
self.search_button.grid(row=0, column=2)
self.download_status_label = tk.Label(self.download_frame, text="Enter URL to download:")
self.download_status_label.grid(row=0, column=0)
self.download_entry = tk.Entry(self.download_frame)
self.download_entry.grid(row=0,column=1)
self.download_button = tk.Button(self.download_frame, text="Download", command=lambda: self.handle_download(self.download_entry.get()))
self.download_button.grid(row=0, column=2)
# Create the bottom part of the UI with tabs
self.bottom_frame = ttk.Frame(self.paned_window)
self.paned_window.add(self.bottom_frame)
self.notebook = ttk.Notebook(self.bottom_frame)
self.notebook.pack(fill='both', expand=True)
self.create_search_tab()
self.create_database_tab(db)
def create_search_tab(self):
search_tab = ttk.Frame(self.notebook)
self.notebook.add(search_tab, text='Search&Download')
self.status_label = tk.Label(search_tab, text="Ready")
self.status_label.pack()
# Create a frame for displaying search results
self.results_frame = ttk.Frame(search_tab)
self.results_frame.pack()
def create_database_tab(self, db):
database_tab = ttk.Frame(self.notebook)
self.notebook.add(database_tab, text='Database')
tree = ttk.Treeview(database_tab, columns=("title", "artist", "filename", "tags", "timestamp"))
tree.heading("#0", text="ID")
tree.heading("title", text="Title")
tree.heading("artist", text="Artist")
tree.heading("filename", text="Filename")
tree.heading("tags", text="Tags")
tree.heading("timestamp", text="Timestamp")
# Populate the treeview with data from the database
for entry in db.get_all_entries():
tree.insert("", "end", text=entry["id"], values=(entry["title"], entry["artist"], entry["filename"], ", ".join(entry["tags"]), entry["timestamp"]))
tree.pack(fill='both', expand=True)
def search_and_display(self):
self.set_status("searching")
query = self.search_entry.get()
# Clear the existing results
for widget in self.results_frame.winfo_children():
widget.destroy()
results = search_youtube(query)
self.display_results(results)
self.set_status("Enter search query or select item to download")
def set_status(self, status):
self.status_label.config(text=status)
self.root.update() # Force GUI update
# Modify the display_results method to use the results_frame in the search_tab
def display_results(self, results):
for i, result in enumerate(results):
if self.db.downloadable(result['webpage_url'], result['title']):
result_button = ttk.Button(self.results_frame, text=result['title'], command=lambda url=result['webpage_url'], filename=result['title']: self.handle_download(url, filename))
result_button.pack()
else:
result_button = tk.Label(self.results_frame, text=f"celaigia: {result['title']}")
result_button.pack()
def handle_download(self, url, filename):
if self.db.downloadable(url, filename):
self.set_status("Downloading...")
success = download_audio(url, self.db.base_path)
if success:
self.set_status("Download completed, adding to db")
self.prompt_user_for_details(url, filename)
self.set_status("Enter search query or select item to download")
# Modify the prompt_user_for_details method
def prompt_user_for_details(self,url, filename):
title = sd.askstring("Enter Title", "Enter Title:", initialvalue=filename)
artist = sd.askstring("Enter Artist", "Enter Artist:")
tags = sd.askstring("Enter Tags", "Enter Tags (comma separated):")
entry = {
"id": url.split('https://www.youtube.com/watch?v=')[-1].strip(),
"title": title.strip(),
"artist": artist.strip(),
"filename": filename.strip(),
"tags": [tag.strip() for tag in tags.split(",")] if tags else [],
"timestamp": time.time()
}
self.add_to_db(entry)
def add_to_db(self, entry):
self.db.add_entry(entry)
def run(self):
self.root.mainloop()

@ -0,0 +1,28 @@
import yt_dlp
from config import yt_dlp_opts
# Function to search YouTube
def search_youtube(query, opts = yt_dlp_opts):
with yt_dlp.YoutubeDL(opts) as ydl:
info = ydl.extract_info(query, download=False)
return info['entries']
def download_audio(url, base_path, opts = yt_dlp_opts):
opts['outtmpl'] = f'{base_path}/audios/%(title)s.%(ext)s'
if not isinstance(url, list):
url = [url]
success = False
try:
with yt_dlp.YoutubeDL(opts) as ydl:
ydl.download(url)
success = True
except:
success = False
return success
Loading…
Cancel
Save