import subprocess import tkinter as tk from tkinter import ttk, messagebox, scrolledtext from pynput import keyboard import threading import requests from datetime import datetime import os import json from urllib.parse import quote class ADBAudioControl: def __init__(self): self.window = tk.Tk() self.window.title("Poweramp Remote") self.window.geometry("500x800") self.window.configure(bg='#1a1a1a') # Initialize variables self.ip_address = tk.StringVar(value="192.168.0.101") self.port = tk.StringVar(value="5555") self.status_text = tk.StringVar(value="Ready") self.connection_status = tk.StringVar(value="Not Connected") self.files_list = [] self.selected_file_path = tk.StringVar(value="") # Set up keyboard listener self.keyboard_listener = keyboard.Listener(on_press=self.handle_key_press) self.keyboard_listener.start() self.setup_ui() self.auto_fetch_ip() def setup_ui(self): # Main container with padding main_frame = tk.Frame(self.window, bg='#1a1a1a', padx=20, pady=20) main_frame.pack(fill=tk.BOTH, expand=True) # Title title_label = tk.Label(main_frame, text="Poweramp Remote", font=('Arial', 24, 'bold'), fg='white', bg='#1a1a1a') title_label.pack(pady=(0, 2)) # Connection Status status_frame = tk.Frame(main_frame, bg='#2d2d2d', padx=10, pady=10) status_frame.pack(fill=tk.X, pady=(0, 10)) status_label = tk.Label(status_frame, text="Connection Status:", font=('Arial', 10), fg='#cccccc', bg='#2d2d2d') status_label.pack(side=tk.LEFT) self.connection_status_label = tk.Label(status_frame, textvariable=self.connection_status, font=('Arial', 10, 'bold'), fg='#ff6b6b', bg='#2d2d2d') self.connection_status_label.pack(side=tk.RIGHT) # IP Address Section ip_frame = tk.Frame(main_frame, bg='#1a1a1a') ip_frame.pack(fill=tk.X, pady=(0, 15)) ip_label = tk.Label(ip_frame, text="IP Address:", font=('Arial', 11), fg='white', bg='#1a1a1a') ip_label.pack(anchor=tk.W) ip_entry = tk.Entry(ip_frame, textvariable=self.ip_address, font=('Arial', 11), bg='#333333', fg='white', insertbackground='white', relief=tk.FLAT) ip_entry.pack(fill=tk.X, pady=(5, 0), ipady=8) # Port Section port_frame = tk.Frame(main_frame, bg='#1a1a1a') port_frame.pack(fill=tk.X, pady=(0, 30)) port_label = tk.Label(port_frame, text="Port:", font=('Arial', 11), fg='white', bg='#1a1a1a') port_label.pack(anchor=tk.W) port_entry = tk.Entry(port_frame, textvariable=self.port, font=('Arial', 11), bg='#333333', fg='white', insertbackground='white', relief=tk.FLAT) port_entry.pack(fill=tk.X, pady=(5, 0), ipady=8) # Control Buttons Frame control_frame = tk.Frame(main_frame, bg='#1a1a1a') control_frame.pack(pady=(0, 20)) # Control Buttons button_style = { 'font': ('Arial', 12, 'bold'), 'bg': '#404040', 'fg': 'white', 'activebackground': '#505050', 'activeforeground': 'white', 'relief': tk.FLAT, 'padx': 10, 'pady': 5, 'width': 5 } # Previous Album Button self.prev_album_button = tk.Button(control_frame, text="⏪", command=lambda: self.send_adb_command(6), **button_style) self.prev_album_button.grid(row=0, column=0, padx=2, pady=5) # Previous Track Button self.prev_button = tk.Button(control_frame, text="⏮", command=lambda: self.send_adb_command(5), **button_style) self.prev_button.grid(row=0, column=1, padx=2, pady=5) # Play/Pause Button self.play_pause_button = tk.Button(control_frame, text="⏯", command=lambda: self.send_adb_command(1), **button_style) self.play_pause_button.grid(row=0, column=2, padx=2, pady=5) # Next Track Button self.next_button = tk.Button(control_frame, text="⏭", command=lambda: self.send_adb_command(4), **button_style) self.next_button.grid(row=0, column=3, padx=2, pady=5) # Next Album Button self.next_album_button = tk.Button(control_frame, text="⏩", command=lambda: self.send_adb_command(7), **button_style) self.next_album_button.grid(row=0, column=4, padx=2, pady=5) # File Picker Section file_picker_frame = tk.LabelFrame(main_frame, text="File Picker", font=('Arial', 11, 'bold'), bg='#1a1a1a', fg='white', padx=10, pady=10) file_picker_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 20)) # Load Files Button load_files_button = tk.Button(file_picker_frame, text="📂 Load Music Files", command=self.load_music_files, font=('Arial', 10), bg='#2d2d2d', fg='white', activebackground='#3d3d3d', activeforeground='white', relief=tk.FLAT, padx=15, pady=8) load_files_button.pack(pady=(0, 10)) # Files Listbox with Scrollbar listbox_frame = tk.Frame(file_picker_frame, bg='#1a1a1a') listbox_frame.pack(fill=tk.BOTH, expand=True) scrollbar = tk.Scrollbar(listbox_frame, bg='#333333') scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.files_listbox = tk.Listbox(listbox_frame, yscrollcommand=scrollbar.set, bg='#333333', fg='white', selectbackground='#4CAF50', selectforeground='white', font=('Arial', 9), height=6) self.files_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.files_listbox.bind('<>', self.on_file_select) scrollbar.config(command=self.files_listbox.yview) # Selected File Info selected_frame = tk.Frame(file_picker_frame, bg='#1a1a1a') selected_frame.pack(fill=tk.X, pady=(10, 5)) selected_label = tk.Label(selected_frame, text="Selected:", font=('Arial', 9), fg='#cccccc', bg='#1a1a1a') selected_label.pack(side=tk.LEFT) self.selected_file_label = tk.Label(selected_frame, textvariable=self.selected_file_path, font=('Arial', 9), fg='#4CAF50', bg='#1a1a1a', wraplength=350) self.selected_file_label.pack(side=tk.LEFT, padx=(5, 0)) # Send Button send_button = tk.Button(file_picker_frame, text="▶ Play Selected File", command=self.send_selected_file, font=('Arial', 11, 'bold'), bg='#4CAF50', fg='white', activebackground='#45a049', activeforeground='white', relief=tk.FLAT, padx=20, pady=10) send_button.pack(pady=(5, 0)) # Restart Button self.restart_button = tk.Button(main_frame, text="🔄 Restart ADB", command=self.restart, font=('Arial', 11), bg='#2d2d2d', fg='white', activebackground='#3d3d3d', activeforeground='white', relief=tk.FLAT, padx=20, pady=12) self.restart_button.pack(pady=(0, 10)) # Fetch IP Button self.fetch_button = tk.Button(main_frame, text="📡 Fetch IP Automatically", command=self.auto_fetch_ip, font=('Arial', 11), bg='#2d2d2d', fg='white', activebackground='#3d3d3d', activeforeground='white', relief=tk.FLAT, padx=20, pady=12) self.fetch_button.pack(pady=(0, 20)) # Status Frame status_display_frame = tk.Frame(main_frame, bg='#2d2d2d', padx=15, pady=15) status_display_frame.pack(fill=tk.X) status_title = tk.Label(status_display_frame, text="Status", font=('Arial', 10, 'bold'), fg='#cccccc', bg='#2d2d2d') status_title.pack(anchor=tk.W) self.status_label = tk.Label(status_display_frame, textvariable=self.status_text, font=('Arial', 10), fg='#4CAF50', bg='#2d2d2d', justify=tk.LEFT, wraplength=400) self.status_label.pack(anchor=tk.W, pady=(5, 0)) # Key Bindings Info keybind_frame = tk.Frame(main_frame, bg='#1a1a1a', padx=10, pady=10) keybind_frame.pack(fill=tk.X, pady=(10, 0)) keybind_title = tk.Label(keybind_frame, text="Media Key Bindings:", font=('Arial', 10, 'bold'), fg='#cccccc', bg='#1a1a1a') keybind_title.pack(anchor=tk.W) keybind_info = tk.Label(keybind_frame, text="• Play/Pause: Media Play/Pause Key\n• Next Track: Media Next Key\n• Previous Track: Media Previous Key\n• Next Album: Key 7\n• Previous Album: Key 6", font=('Arial', 9), fg='#999999', bg='#1a1a1a', justify=tk.LEFT) keybind_info.pack(anchor=tk.W, pady=(5, 0)) # Version/Attribution attribution = tk.Label(main_frame, text="Poweramp Remote", font=('Arial', 8), fg='#666666', bg='#1a1a1a') attribution.pack(side=tk.BOTTOM, pady=(10, 0)) # Bind keyboard events self.window.bind("", self.on_key_press) def encode_uri_like_js(self, uri): """Encode URI like JavaScript's encodeURI function""" # encodeURI doesn't encode: A-Z a-z 0-9 ; , / ? : @ & = + $ - _ . ! ~ * ' ( ) # # It does encode spaces and other special characters # List of characters that should NOT be encoded safe_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789;,/?:@&=+$-_.!~*'()#" encoded_parts = [] for char in uri: if char in safe_chars: encoded_parts.append(char) elif char == ' ': encoded_parts.append('%20') elif char == "'": encoded_parts.append("%27") else: # URL encode other characters encoded_parts.append(quote(char, safe='')) return ''.join(encoded_parts) def load_music_files(self): """Load music files from the remote URL""" list_url = 'https://alcea-wisteria.de/PHP/0demo/2025-12-22-DeviceNAppCtrl/[Android]_Poweramp%E2%80%90(Tasker)(20251222)/0ld/list.txt' def fetch_files(): try: self.update_status("Loading music files list...") response = requests.get(list_url, timeout=10) if response.status_code == 200: lines = response.text.strip().split('\n') self.files_list = [line.strip() for line in lines if line.strip()] # Clear and update listbox self.files_listbox.delete(0, tk.END) for file_path in self.files_list: # Extract display name display_name = self.extract_display_name(file_path) self.files_listbox.insert(tk.END, display_name) self.update_status(f"Loaded {len(self.files_list)} music files") else: self.update_status(f"Failed to load files. Status code: {response.status_code}", "error") except requests.exceptions.RequestException as e: self.update_status(f"Network error: {str(e)}", "error") except Exception as e: self.update_status(f"Error loading files: {str(e)}", "error") # Run in separate thread threading.Thread(target=fetch_files, daemon=True).start() def extract_display_name(self, file_path): """Extract display name from file path (matches HTML logic)""" if '/mobile/' in file_path: parts = file_path.split('/mobile/') if len(parts) > 1: return parts[1] # Fallback to last 2 parts of path path_parts = [part for part in file_path.split('/') if part.strip() != ''] return '/'.join(path_parts[-2:]) if len(path_parts) >= 2 else file_path def convert_file_path(self, file_path): """Convert file path from /mnt/media_rw to /sdcard (matches HTML logic)""" return file_path.replace('/mnt/media_rw', '/sdcard') def on_file_select(self, event): """Handle file selection from listbox""" selection = self.files_listbox.curselection() if selection: index = selection[0] if 0 <= index < len(self.files_list): original_path = self.files_list[index] display_name = self.extract_display_name(original_path) self.selected_file_path.set(display_name) # Store the full path in the widget for later use self.files_listbox.selected_index = index def send_selected_file(self): """Send selected file to Poweramp via ADB""" if not hasattr(self.files_listbox, 'selected_index'): self.update_status("No file selected", "error") return index = self.files_listbox.selected_index if 0 <= index < len(self.files_list): original_path = self.files_list[index] self.play_file_via_adb(original_path) def play_file_via_adb(self, original_path): """Play a file via ADB using EXACTLY the method from the HTML""" try: ip_address = self.ip_address.get() port = self.port.get() if not ip_address or not port: self.update_status("Error: IP and Port must be specified", "error") return # Convert path as shown in HTML (exactly the same logic) android_path = self.convert_file_path(original_path) # Create file URL exactly like HTML file_url = f"file://{android_path}" # Encode exactly like JavaScript's encodeURI (from HTML) encoded_file_url = self.encode_uri_like_js(file_url) # Create the ADB command EXACTLY like in HTML adb_command = f'am start -n com.maxmpz.audioplayer/.PlayerUIActivity -a android.intent.action.VIEW -d "{encoded_file_url}" -t audio/mpeg' # Full ADB command with device full_adb_command = f'adb -s {ip_address}:{port} shell "{adb_command}"' display_name = self.extract_display_name(original_path) self.update_status(f"Sending file to Poweramp: {display_name}...") # Debug output print(f"DEBUG - Original path: {original_path}") print(f"DEBUG - Android path: {android_path}") print(f"DEBUG - File URL: {file_url}") print(f"DEBUG - Encoded URL: {encoded_file_url}") print(f"DEBUG - ADB Command: {full_adb_command}") def run_command(): try: # First try with double quotes result = subprocess.run(full_adb_command, shell=True, capture_output=True, text=True, timeout=10) if result.returncode == 0: self.update_status(f"Playing: {display_name}") self.connection_status.set("Connected") self.connection_status_label.config(fg='#4CAF50') print(f"DEBUG - Success! Output: {result.stdout}") else: # Try alternative: single quotes for shell command alt_command = f'adb -s {ip_address}:{port} shell \'{adb_command}\'' print(f"DEBUG - Trying alternative: {alt_command}") alt_result = subprocess.run(alt_command, shell=True, capture_output=True, text=True, timeout=10) if alt_result.returncode == 0: self.update_status(f"Playing: {display_name}") self.connection_status.set("Connected") self.connection_status_label.config(fg='#4CAF50') print(f"DEBUG - Alternative success! Output: {alt_result.stdout}") else: error_msg = alt_result.stderr if alt_result.stderr else alt_result.stdout self.update_status(f"Error: {error_msg[:100]}...", "error") self.connection_status.set("Connection Failed") print(f"DEBUG - Alternative failed: {error_msg}") except subprocess.TimeoutExpired: self.update_status("Command timed out", "error") except Exception as e: self.update_status(f"Error: {str(e)}", "error") print(f"DEBUG - Exception: {str(e)}") threading.Thread(target=run_command, daemon=True).start() except Exception as e: self.update_status(f"Error: {str(e)}", "error") print(f"DEBUG - Outer exception: {str(e)}") def send_adb_command(self, command): """Send standard Poweramp API commands""" try: ip_address = self.ip_address.get() port = self.port.get() if not ip_address or not port: self.update_status("Error: IP and Port must be specified", "error") return adb_shell_command = f"adb -s {ip_address}:{port} shell am broadcast 'intent:#Intent;action=com.maxmpz.audioplayer.API_COMMAND;package=com.maxmpz.audioplayer;i.cmd={command};end'" # Update status commands = { 1: "Play/Pause", 4: "Next Track", 5: "Previous Track", 6: "Previous Album", 7: "Next Album" } self.update_status(f"Sending command: {commands.get(command, 'Unknown')}...") # Run command in thread to prevent UI freezing def run_command(): try: result = subprocess.run(adb_shell_command, shell=True, capture_output=True, text=True) if result.returncode == 0: self.update_status(f"Command sent successfully: {commands.get(command, 'Unknown')}") self.connection_status.set("Connected") self.connection_status_label.config(fg='#4CAF50') else: self.update_status(f"Error sending command: {result.stderr}", "error") self.connection_status.set("Connection Failed") except Exception as e: self.update_status(f"Error: {str(e)}", "error") threading.Thread(target=run_command, daemon=True).start() except Exception as e: self.update_status(f"Error: {str(e)}", "error") def restart(self): """Restart ADB server""" try: ip_address = self.ip_address.get() port = self.port.get() if not ip_address or not port: self.update_status("Error: IP and Port must be specified", "error") return command = f'adb kill-server && adb start-server && adb connect {ip_address}:{port}' self.update_status("Restarting ADB server...") def run_restart(): try: subprocess.Popen(['cmd.exe', '/c', 'start', 'cmd.exe', '/k', command]) self.update_status("ADB restart initiated in new terminal") except Exception as e: self.update_status(f"Error restarting ADB: {str(e)}", "error") threading.Thread(target=run_restart, daemon=True).start() except Exception as e: self.update_status(f"Error: {str(e)}", "error") def auto_fetch_ip(self): """Fetch IP from the remote URL""" url = "https://alcea-wisteria.de/PHP/0demo/2025-12-22-DeviceNAppCtrl/%5BAndroid%5D_General-Fetch-DeviceIP-%2820251223%29/address.txt" def fetch(): try: self.update_status("Fetching IP address from remote server...") response = requests.get(url, timeout=10) if response.status_code == 200: ip = response.text.strip() if ip: self.ip_address.set(ip) self.update_status(f"IP fetched successfully: {ip}") else: self.update_status("No IP address found in response", "warning") else: self.update_status(f"Failed to fetch IP. Status code: {response.status_code}", "error") except requests.exceptions.RequestException as e: self.update_status(f"Network error: {str(e)}", "error") except Exception as e: self.update_status(f"Error: {str(e)}", "error") # Run in separate thread threading.Thread(target=fetch, daemon=True).start() def update_status(self, message, msg_type="info"): """Update status message with color coding""" colors = { "info": "#4CAF50", "error": "#ff6b6b", "warning": "#ffa726" } timestamp = datetime.now().strftime("%H:%M:%S") self.status_text.set(f"[{timestamp}] {message}") self.status_label.config(fg=colors.get(msg_type, "#4CAF50")) # Auto-clear info messages after 5 seconds if msg_type == "info": self.window.after(5000, lambda: self.clear_status_if_old(timestamp)) def clear_status_if_old(self, timestamp): """Clear status if it's still showing the old timestamp""" current_status = self.status_text.get() if f"[{timestamp}]" in current_status: self.status_text.set("Ready") self.status_label.config(fg="#4CAF50") def handle_key_press(self, key): """Handle media key presses""" try: if key == keyboard.Key.media_play_pause: self.send_adb_command(1) elif key == keyboard.Key.media_next: self.send_adb_command(4) elif key == keyboard.Key.media_previous: self.send_adb_command(5) elif key == keyboard.Key.media_previous_album: self.send_adb_command(6) elif key == keyboard.Key.media_next_album: self.send_adb_command(7) except: pass def on_key_press(self, event): """Handle keyboard events in Tkinter window""" key = event.keysym.lower() if key == 'xf86audioplay': self.send_adb_command(1) elif key == 'xf86audionext': self.send_adb_command(4) elif key == 'xf86audioprev': self.send_adb_command(5) elif key == 'xf86audioalbumnext': self.send_adb_command(7) elif key == 'xf86audioalbumprev': self.send_adb_command(6) def run(self): self.window.mainloop() def __del__(self): if hasattr(self, 'keyboard_listener'): self.keyboard_listener.stop() if __name__ == "__main__": app = ADBAudioControl() app.run()