search button fixes, minimal player in database tab

main
agropunx 11 months ago
parent 38f55b5b56
commit 6b5fb24556

1
.gitignore vendored

@ -6,6 +6,7 @@
*.ipynb
*.idea
music/
# Byte-compiled / optimized / DLL files
__pycache__/

@ -1,9 +1,27 @@
Welcome to celaigia
# celaigia
an app to gently query youtube music, and possibly download only if you don't have already locally.
## Features
- Search and download music from YouTube
- Manage available music in a database
- Play, pausemusic from the database
## Usage
To use Celaigia, follow these steps:
0. git clone the repo: `git clone https://git.tropici.net/agropunx/celaigia && cd celaigia`
1. install dependencies: `pip install -r requirements.txt`.
2. run the app by executing: `python app.py`.
3. use the search & download tab to find music on YouTube and download it if needed.
4. manage available music in the database tab, where you can play, pause music (by double click).
## Requirements
- Python 3.x
- Dependencies listed in `requirements.txt`
## TODO
- search & download tab: flag to download video
- database tab: play / pause / remove buttons
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

@ -3,7 +3,6 @@ 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,11 +1,22 @@
name = 'celaigia'
ui_size = '500x300'
music_path = "./music"
audio_codec = 'mp3'
youtube_prefix = 'https://www.youtube.com/watch?v='
yt_dlp_opts = {
'format': 'bestaudio/best',
'default_search': 'ytsearch5',
'postprocessors': [{
'key': 'FFmpegExtractAudio',
'preferredcodec': 'mp3',
'preferredcodec': audio_codec,
'preferredquality': '192',
}],
}
#https://www.askpython.com/python-modules/tkinter/tkinter-colors
colors = {
'DownloadButton': 'Turquoise',
'SearchButton':'DeepSkyBlue',
}

@ -1,6 +1,6 @@
# Updated database.py
import json
import datetime
from config import youtube_prefix
class Database:
def __init__(self, base_path):
@ -18,10 +18,10 @@ class Database:
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]
def downloadable(self, url, title=None):
idx = url.split(youtube_prefix)[-1]
for entry in self.data:
if entry['id'] == idx or entry['title'] == title:
if entry['id'] == idx or (title is not None and entry['title'] == title):
return False
return True

@ -98,5 +98,94 @@
"italiano"
],
"timestamp": "31/12/2023"
},
{
"id": "vtCk4sNgj48",
"title": "Tuve que quemar",
"artist": "sara hebe",
"filename": "Tuve que quemar",
"tags": [
"latin",
"rap",
"queer"
],
"timestamp": "01/01/2024"
},
{
"id": "bgkFHSOaTsk",
"title": "red and gold",
"artist": "mfdoom",
"filename": "",
"tags": [
"rap",
"hip hop"
],
"timestamp": "01/01/2024"
},
{
"id": "h9TlaYxoOO8",
"title": "Los Ageless",
"artist": "stvincent",
"filename": "St. Vincent - \"Los Ageless\" (Official Video)",
"tags": [
"rock",
"indie"
],
"timestamp": "01/01/2024"
},
{
"id": "G8jxAKmojxY",
"title": "Los Ageless2",
"artist": "stvincent",
"filename": "St. Vincent - Los Ageless (official audio)",
"tags": [
"rock",
"female",
"indie"
],
"timestamp": "01/01/2024"
},
{
"id": "CTg4WKcOJ3g",
"title": "los ageless live",
"artist": "stvincent",
"filename": "St. Vincent Performs 'Los Ageless'",
"tags": [
"live"
],
"timestamp": "01/01/2024"
},
{
"id": "KogCed9gwxk",
"title": "Los Ageless lyrics",
"artist": "stvincent",
"filename": "St. Vincent - Los Ageless (Lyrics)",
"tags": [
"karaoke"
],
"timestamp": "01/01/2024"
},
{
"id": "mQn_jPaC6mQ",
"title": "I'm Still In Love",
"artist": "Alton Ellis",
"filename": "I'm Still In Love",
"tags": [
"ska",
"original",
"jamaica"
],
"timestamp": "01/01/2024"
},
{
"id": "5sqdtBTXVpI",
"title": "Cleaner Than Your Surroundings",
"artist": "Lungfish",
"filename": "Lungfish - Cleaner Than Your Surroundings",
"tags": [
"post hardcore",
"punk"
],
"timestamp": "01/01/2024"
}
]

@ -1 +1,2 @@
yt-dlp
pygame

194
ui.py

@ -3,135 +3,157 @@ from tkinter import ttk
import tkinter.simpledialog as sd
import time
import os
import pygame
from config import name, ui_size,colors, youtube_prefix, audio_codec
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
self.root.title(name)
self.root.geometry(ui_size)
self.status_label = tk.Label(self.root, text="ready")
self.status_label.pack()
self.notebook = ttk.Notebook(self.root)
self.notebook.pack(fill='both', expand=True)
self.create_search_tab()
self.create_database_tab(db)
pygame.init()
# 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')
def set_status(self, status):
self.status_label.config(text=status)
self.root.update()
# Create the top part of the UI
self.top_frame = ttk.Frame(self.paned_window)
self.paned_window.add(self.top_frame)
def add_to_db(self, entry):
self.db.add_entry(entry)
self.status_label = tk.Label(self.top_frame, text="Ready")
self.status_label.pack()
def run(self):
self.root.mainloop()
self.search_frame = ttk.Frame(self.top_frame)
self.search_frame.pack()
self.download_frame = ttk.Frame(self.top_frame)
self.download_frame.pack()
def create_search_tab(self):
search_tab = ttk.Frame(self.notebook)
self.notebook.add(search_tab, text='search & download')
self.search_status_label = tk.Label(self.search_frame, text="Enter search query:")
self.search_status_label.grid(row=0, column=0)
# search_frame
self.search_frame = ttk.Frame(search_tab)
self.search_label = tk.Label(self.search_frame, text="search youtube videos:")
self.search_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 = tk.Button(
self.search_frame,
text="Search",
background=colors['SearchButton'],
command=lambda: self.search_and_display()
)
self.search_button.grid(row=0, column=2)
self.search_frame.pack(side='top')
self.download_status_label = tk.Label(self.download_frame, text="Enter URL to download:")
self.download_status_label.grid(row=0, column=0)
# results frame
self.results_frame = ttk.Frame(search_tab)
self.results_frame.pack()
# download frame
self.download_frame = ttk.Frame(search_tab)
self.download_label = tk.Label(self.download_frame, text="...or directly enter a youtube URL to download:")
self.download_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 = tk.Button(
self.download_frame,
text="Download",
background=colors['DownloadButton'],
command=lambda: self.handle_download(self.download_entry.get())
)
self.download_button.grid(row=0, column=2)
self.download_frame.pack(side='bottom')
# 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 play_selected_music(self, event):
item = self.music_tree.selection()[0]
filename = f"{self.db.base_path}/audios/{self.music_tree.item(item, 'values')[2]}.{audio_codec}"
if pygame.mixer.music.get_busy() and pygame.mixer.music.get_pos() > 0:
pygame.mixer.music.stop()
else:
pygame.mixer.music.load(filename)
pygame.mixer.music.play()
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
self.database_tab = ttk.Frame(self.notebook)
self.notebook.add(self.database_tab, text='available music')
self.music_tree = ttk.Treeview(self.database_tab, columns=("title", "artist", "filename", "tags", "timestamp"))
self.music_tree.heading("#0", text="ID")
self.music_tree.heading("title", text="Title")
self.music_tree.heading("artist", text="Artist")
self.music_tree.heading("filename", text="Filename")
self.music_tree.heading("tags", text="Tags")
self.music_tree.heading("timestamp", text="Timestamp")
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)
self.music_tree.insert("", "end", text=entry["id"], values=(entry["title"], entry["artist"], entry["filename"], ", ".join(entry["tags"]), entry["timestamp"]))
self.music_tree.bind("<Double-1>", self.play_selected_music)
self.music_tree.pack(fill='both', expand=True)
def update_database_tab(self, entry):
tree = self.notebook.winfo_children()[1].winfo_children()[0] # Access the treeview in the database tab
tree.insert("", "end", text=entry["id"], values=(entry["title"], entry["artist"], entry["filename"], ", ".join(entry["tags"]), entry["timestamp"]))
def search_and_display(self):
self.set_status("searching")
query = self.search_entry.get()
# Clear the existing results
self.set_status(f"searching {query} on youtube...")
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):
result_label = tk.Label(self.results_frame, text="choose from the list and click to download:")
result_label.pack()
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']}")
text = result['title']
state = 'active'
if not self.db.downloadable(result['webpage_url'], result['title']):
text = f'celaigia : {text}'
state = 'disabled'
result_button = tk.Button(
self.results_frame,
text=text,
background=colors['DownloadButton'],
command=lambda url=result['webpage_url'], filename=result['title']: self.handle_download(url, filename)
)
result_button.pack()
result_button.configure(state=state)
self.set_status("ready")
def handle_download(self, url, filename):
if self.db.downloadable(url, filename):
self.set_status("Downloading...")
def handle_download(self, url, filename=None):
local_path = f"{self.db.base_path}/audios/{filename}.{audio_codec}"
self.set_status(f"downloading to {local_path} ...")
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")
self.set_status("download completed, waiting for user prompt and then adding to db")
try:
entry = self.prompt_user_for_details(url, filename)
self.add_to_db(entry)
self.update_database_tab(entry)
for widget in self.results_frame.winfo_children():
if widget['text'] == entry['filename']:
widget['text'] = f"celaigia : {widget['text']}"
widget.configure(state='disabled')
except:
if os.path.exists(local_path):
os.remove(local_path)
self.set_status("ready")
# Modify the prompt_user_for_details method
def prompt_user_for_details(self,url, filename):
if filename is None:
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(),
"id": url.split(youtube_prefix)[-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()
return entry

@ -2,7 +2,6 @@ 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)

Loading…
Cancel
Save