mytunes-pro 2.0.2__py3-none-any.whl → 2.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mytunes/app.py +171 -299
- {mytunes_pro-2.0.2.dist-info → mytunes_pro-2.0.4.dist-info}/METADATA +57 -111
- mytunes_pro-2.0.4.dist-info/RECORD +8 -0
- mytunes_pro-2.0.2.dist-info/RECORD +0 -8
- {mytunes_pro-2.0.2.dist-info → mytunes_pro-2.0.4.dist-info}/WHEEL +0 -0
- {mytunes_pro-2.0.2.dist-info → mytunes_pro-2.0.4.dist-info}/entry_points.txt +0 -0
- {mytunes_pro-2.0.2.dist-info → mytunes_pro-2.0.4.dist-info}/licenses/LICENSE +0 -0
- {mytunes_pro-2.0.2.dist-info → mytunes_pro-2.0.4.dist-info}/top_level.txt +0 -0
mytunes/app.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
3
|
MyTunes Pro - Professional TUI Edition v1.0
|
|
4
|
-
Premium CLI
|
|
4
|
+
# Premium CLI Media Workflow Experiment with Curses Interface
|
|
5
5
|
Enhanced with Context7-researched MPV IPC & Resize Handling
|
|
6
6
|
"""
|
|
7
7
|
import curses
|
|
@@ -32,7 +32,7 @@ MPV_SOCKET = "/tmp/mpv_socket"
|
|
|
32
32
|
LOG_FILE = "/tmp/mytunes_mpv.log"
|
|
33
33
|
PID_FILE = "/tmp/mytunes_mpv.pid"
|
|
34
34
|
APP_NAME = "MyTunes Pro"
|
|
35
|
-
APP_VERSION = "2.0.
|
|
35
|
+
APP_VERSION = "2.0.4"
|
|
36
36
|
|
|
37
37
|
# === [Strings & Localization] ===
|
|
38
38
|
STRINGS = {
|
|
@@ -52,10 +52,10 @@ STRINGS = {
|
|
|
52
52
|
"fav_added": "★ 즐겨찾기에 추가됨",
|
|
53
53
|
"fav_removed": "☆ 즐겨찾기 해제됨",
|
|
54
54
|
"header_r1": "[S/1]검색 [F/2]즐겨찾기 [R/3]기록 [M/4]메인 [A/5]즐겨찾기추가 [Q/6]뒤로",
|
|
55
|
-
"header_r2": "[F7]유튜브 [
|
|
56
|
-
"help_guide": "[j/k]이동 [En]선택 [h/q]뒤로 [S/1]검색 [F/2]즐겨찾기 [R/3]기록 [M/4]메인 [F7]유튜브
|
|
55
|
+
"header_r2": "[F7]유튜브 [SPC]Play/Stop [+/-]볼륨 [<>]빨리감기 [D/Del]삭제",
|
|
56
|
+
"help_guide": "[j/k]이동 [En]선택 [h/q]뒤로 [S/1]검색 [F/2]즐겨찾기 [R/3]기록 [M/4]메인 [F7]유튜브",
|
|
57
57
|
"menu_main": "☰ 메인 메뉴",
|
|
58
|
-
"menu_search_results": "⌕
|
|
58
|
+
"menu_search_results": "⌕ 미디어 콘텐츠 검색",
|
|
59
59
|
"menu_favorites": "★ 나의 즐겨찾기",
|
|
60
60
|
"menu_history": "◷ 재생 기록",
|
|
61
61
|
"menu_bg_play": "⧉ 백그라운드 재생 (나가기)",
|
|
@@ -81,10 +81,10 @@ STRINGS = {
|
|
|
81
81
|
"fav_added": "★ Added to Favorites",
|
|
82
82
|
"fav_removed": "☆ Removed from Favorites",
|
|
83
83
|
"header_r1": "[S/1]Srch [F/2]Favs [R/3]Hist [M/4]Main [A/5]AddFav [Q/6]Back",
|
|
84
|
-
"header_r2": "[F7]YT [
|
|
85
|
-
"help_guide": "[j/k]Move [En]Select [h/q]Back [S/1]Srch [F/2]Fav [R/3]Hist [M/4]Main [F7]YT
|
|
84
|
+
"header_r2": "[F7]YT [SPC]Play/Stop [+/-]Vol [<>]Seek [D/Del]Del",
|
|
85
|
+
"help_guide": "[j/k]Move [En]Select [h/q]Back [S/1]Srch [F/2]Fav [R/3]Hist [M/4]Main [F7]YT",
|
|
86
86
|
"menu_main": "☰ Main Menu",
|
|
87
|
-
"menu_search_results": "⌕ Search
|
|
87
|
+
"menu_search_results": "⌕ Search Media Content",
|
|
88
88
|
"menu_favorites": "★ My Favorites",
|
|
89
89
|
"menu_history": "◷ History",
|
|
90
90
|
"menu_bg_play": "⧉ Background Play (Leave)",
|
|
@@ -311,7 +311,7 @@ class Player:
|
|
|
311
311
|
# 'accessory' hides Dock but allows system resources
|
|
312
312
|
cmd.append("--macos-app-activation-policy=accessory")
|
|
313
313
|
|
|
314
|
-
# C.
|
|
314
|
+
# C. Media Source 403 Forbidden Bypass (Cross-platform robustness)
|
|
315
315
|
# This uses the Android player client which is currently the most stable
|
|
316
316
|
# and avoids HLS segment blocks on both Linux and macOS.
|
|
317
317
|
cmd.extend([
|
|
@@ -467,9 +467,6 @@ class MyTunesApp:
|
|
|
467
467
|
curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION)
|
|
468
468
|
print("\033[?1003h") # Enable mouse tracking
|
|
469
469
|
|
|
470
|
-
# Sharing Client (Serverless Proxy)
|
|
471
|
-
self.pusher = None # Deprecated: Direct Pusher client removed for security
|
|
472
|
-
self.share_api_url = "https://postgresql.co.kr/api/pusher/mytunes"
|
|
473
470
|
self.sent_history = {}
|
|
474
471
|
|
|
475
472
|
|
|
@@ -585,332 +582,191 @@ class MyTunesApp:
|
|
|
585
582
|
m, s = divmod(int(seconds), 60)
|
|
586
583
|
return f"{m:02d}:{s:02d}"
|
|
587
584
|
|
|
588
|
-
def
|
|
585
|
+
def get_next_event(self):
|
|
586
|
+
"""Unified input collection and normalization (Unicode, special keys, macros)."""
|
|
589
587
|
try:
|
|
590
|
-
# Use get_wch for Unicode support (captures Korean shortcuts)
|
|
591
588
|
key = self.stdscr.get_wch()
|
|
592
|
-
except curses.error:
|
|
593
|
-
|
|
594
|
-
except:
|
|
595
|
-
return
|
|
596
|
-
|
|
597
|
-
if key == -1: return
|
|
589
|
+
except curses.error: return None
|
|
590
|
+
except: return None
|
|
598
591
|
|
|
599
|
-
|
|
592
|
+
if key == -1: return None
|
|
600
593
|
self.last_input_time = time.time()
|
|
601
594
|
|
|
602
|
-
|
|
603
|
-
# Handle formatting: invalid key might be int -1
|
|
604
|
-
|
|
605
|
-
# Resize Info
|
|
595
|
+
# 1. Resize handling
|
|
606
596
|
if key == curses.KEY_RESIZE:
|
|
607
597
|
self.stdscr.clear()
|
|
608
598
|
self.stdscr.refresh()
|
|
609
|
-
return
|
|
599
|
+
return "RESIZE"
|
|
610
600
|
|
|
611
|
-
#
|
|
612
|
-
# get_wch returns int 27 or str '\x1b' depending on system/lib
|
|
613
|
-
# v2.0.1 MAC FIX: Check for Option+Backspace (ESC then 127) for Deletion
|
|
601
|
+
# 2. ESC / Combined sequences (Option+Backspace macro)
|
|
614
602
|
if key == 27 or key == '\x1b':
|
|
615
|
-
#
|
|
616
|
-
self.stdscr.timeout(50)
|
|
603
|
+
self.stdscr.timeout(50) # Tiny peek timeout
|
|
617
604
|
try:
|
|
618
|
-
|
|
619
|
-
if
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
# For simplicity, if it's not the sequence we want, we treat ESC as ESC
|
|
625
|
-
# and if we consumed a key, well, generic ESC logic applies.
|
|
626
|
-
# Ideally ungetch if possible, but for now fallback to ESC behavior.
|
|
627
|
-
# But if we consumed a legitimate key user typed fast, that's bad.
|
|
628
|
-
# However, 50ms is very fast.
|
|
629
|
-
if next_key != -1:
|
|
630
|
-
curses.ungetch(next_key)
|
|
631
|
-
|
|
632
|
-
# Proceed with standard ESC behavior
|
|
633
|
-
self.stop_on_exit = False
|
|
634
|
-
self.running = False
|
|
635
|
-
return
|
|
636
|
-
except:
|
|
637
|
-
# Timeout / Error -> Just ESC
|
|
638
|
-
self.stop_on_exit = False
|
|
639
|
-
self.running = False
|
|
640
|
-
return
|
|
641
|
-
finally:
|
|
642
|
-
# Restore timeout
|
|
643
|
-
self.stdscr.timeout(1000 if (time.time() - getattr(self, 'last_input_time', 0) > 60 and self.is_paused) else 200)
|
|
605
|
+
nk = self.stdscr.getch()
|
|
606
|
+
if nk == 127: return "DELETE" # Option+Backspace as DELETE
|
|
607
|
+
if nk != -1: curses.ungetch(nk)
|
|
608
|
+
except: pass
|
|
609
|
+
finally: self.stdscr.timeout(200) # Reset to standard
|
|
610
|
+
return "EXIT_BKG" # Standard ESC
|
|
644
611
|
|
|
645
|
-
#
|
|
612
|
+
# 3. Mouse Click
|
|
646
613
|
if key == curses.KEY_MOUSE:
|
|
647
614
|
try:
|
|
648
615
|
_, mx, my, _, bstate = curses.getmouse()
|
|
649
|
-
if bstate & curses.BUTTON1_CLICKED
|
|
616
|
+
if bstate & (curses.BUTTON1_CLICKED | curses.BUTTON1_RELEASED):
|
|
650
617
|
h, w = self.stdscr.getmaxyx()
|
|
651
618
|
branding = "mytunes-pro.com/postgresql.co.kr"
|
|
652
|
-
|
|
653
|
-
branding_x = w - 2 - branding_len
|
|
654
|
-
|
|
619
|
+
branding_x = w - 2 - len(branding)
|
|
655
620
|
if my == h - 2 and branding_x <= mx < w - 2:
|
|
656
|
-
# Check which part was clicked
|
|
657
|
-
# mytunes-pro.com (first 15 chars)
|
|
658
|
-
# / (1 char)
|
|
659
|
-
# postgresql.co.kr (last 16 chars)
|
|
660
621
|
rel_x = mx - branding_x
|
|
661
|
-
if rel_x < 15:
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
622
|
+
if rel_x < 15: return "OPEN_HOME"
|
|
623
|
+
if rel_x > 15: return "OPEN_PARTNER"
|
|
624
|
+
except: pass
|
|
625
|
+
return "MOUSE_CLICK"
|
|
626
|
+
|
|
627
|
+
# 4. Standard Keys Mapping
|
|
628
|
+
k_char = str(key).lower() if isinstance(key, str) else str(key)
|
|
629
|
+
mapping = {
|
|
630
|
+
str(curses.KEY_LEFT): "NAV_BACK", str(curses.KEY_BACKSPACE): "NAV_BACK", "127": "NAV_BACK",
|
|
631
|
+
"q": "NAV_BACK", "6": "NAV_BACK", "h": "NAV_BACK",
|
|
632
|
+
str(curses.KEY_RIGHT): "NAV_FORWARD", "l": "NAV_FORWARD",
|
|
633
|
+
str(curses.KEY_UP): "MOVE_UP", "k": "MOVE_UP",
|
|
634
|
+
str(curses.KEY_DOWN): "MOVE_DOWN", "j": "MOVE_DOWN",
|
|
635
|
+
"\n": "ACTIVATE", "\r": "ACTIVATE", "10": "ACTIVATE", "13": "ACTIVATE", str(curses.KEY_ENTER): "ACTIVATE",
|
|
636
|
+
"s": "SEARCH", "1": "SEARCH", "/": "SEARCH",
|
|
637
|
+
"f": "FAVORITES", "2": "FAVORITES",
|
|
638
|
+
"r": "HISTORY", "3": "HISTORY",
|
|
639
|
+
"m": "MAIN_MENU", "4": "MAIN_MENU",
|
|
640
|
+
" ": "TOGGLE_PAUSE",
|
|
641
|
+
"-": "VOL_DOWN", "_": "VOL_DOWN",
|
|
642
|
+
"+": "VOL_UP", "=": "VOL_UP",
|
|
643
|
+
",": "SEEK_BACK_10", ".": "SEEK_FWD_10",
|
|
644
|
+
"<": "SEEK_BACK_30", ">": "SEEK_FWD_30",
|
|
645
|
+
"a": "TOGGLE_FAV", "5": "TOGGLE_FAV",
|
|
646
|
+
str(curses.KEY_F7): "OPEN_BROWSER",
|
|
647
|
+
str(curses.KEY_DC): "DELETE", "d": "DELETE"
|
|
648
|
+
}
|
|
649
|
+
return mapping.get(k_char)
|
|
650
|
+
|
|
651
|
+
def handle_input(self):
|
|
652
|
+
"""Clean dispatcher: Get normalized command and execute it."""
|
|
653
|
+
cmd = self.get_next_event()
|
|
654
|
+
if not cmd: return
|
|
670
655
|
|
|
671
|
-
# Helper to normalize input for checking
|
|
672
|
-
k_char = str(key).lower() if isinstance(key, str) else ""
|
|
673
|
-
|
|
674
656
|
current_list = self.get_current_list()
|
|
675
657
|
|
|
676
|
-
#
|
|
677
|
-
|
|
678
|
-
# Fix: Removed Korean mappings ('ㅂ', 'ㅗ') to prevent IME ghost keys per user request
|
|
679
|
-
if key == curses.KEY_LEFT or key == curses.KEY_BACKSPACE or key == 127 or \
|
|
680
|
-
k_char in ['q', '6', 'h']:
|
|
658
|
+
# 1. Functional Commands (Require Logic)
|
|
659
|
+
if cmd == "NAV_BACK":
|
|
681
660
|
if len(self.view_stack) > 1:
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
self.selection_idx = 0; self.scroll_offset = 0
|
|
687
|
-
self.status_msg = ""
|
|
688
|
-
# Else: Do nothing (Prevent Quit on Q)
|
|
689
|
-
return
|
|
690
|
-
|
|
691
|
-
# Forward: L, Right Arrow (Browser Style)
|
|
692
|
-
# Re-visit the view we just popped from
|
|
693
|
-
# Fix: Removed Korean mappings ('ㅣ') to prevent IME ghost keys
|
|
694
|
-
if k_char in ['l', 'L'] or key == curses.KEY_RIGHT:
|
|
661
|
+
self.forward_stack.append(self.view_stack.pop())
|
|
662
|
+
self.selection_idx = 0; self.scroll_offset = 0; self.status_msg = ""
|
|
663
|
+
|
|
664
|
+
elif cmd == "NAV_FORWARD":
|
|
695
665
|
if self.forward_stack:
|
|
696
|
-
|
|
697
|
-
self.
|
|
698
|
-
self.selection_idx = 0; self.scroll_offset = 0
|
|
699
|
-
self.status_msg = ""
|
|
700
|
-
return
|
|
666
|
+
self.view_stack.append(self.forward_stack.pop())
|
|
667
|
+
self.selection_idx = 0; self.scroll_offset = 0; self.status_msg = ""
|
|
701
668
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
# Fix: Removed Korean mappings ('ㅏ', 'ㅓ') for stability
|
|
705
|
-
if key == curses.KEY_UP or k_char in ['k']:
|
|
669
|
+
elif cmd == "MOVE_UP":
|
|
706
670
|
if self.selection_idx > 0:
|
|
707
671
|
self.selection_idx -= 1
|
|
708
672
|
if self.selection_idx < self.scroll_offset: self.scroll_offset = self.selection_idx
|
|
709
673
|
elif current_list:
|
|
710
|
-
# v1.8.5 - Wrapping: Top to Bottom
|
|
711
674
|
self.selection_idx = len(current_list) - 1
|
|
712
675
|
h, _ = self.stdscr.getmaxyx()
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
elif key == curses.KEY_DOWN or k_char in ['j']:
|
|
676
|
+
self.scroll_offset = max(0, self.selection_idx - (h - 11))
|
|
677
|
+
|
|
678
|
+
elif cmd == "MOVE_DOWN":
|
|
717
679
|
if self.selection_idx < len(current_list) - 1:
|
|
718
680
|
self.selection_idx += 1
|
|
719
681
|
h, _ = self.stdscr.getmaxyx()
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
self.scroll_offset = self.selection_idx - list_area_height + 1
|
|
682
|
+
if self.selection_idx >= self.scroll_offset + (h - 10):
|
|
683
|
+
self.scroll_offset = self.selection_idx - (h - 10) + 1
|
|
723
684
|
elif current_list:
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
# Shortcuts: Number keys & English letters (Strict Mode)
|
|
736
|
-
# Search: S, 1, /
|
|
737
|
-
elif k_char in ['s', 'S', '1', '/'] and (not isinstance(key, str) or key.isprintable()):
|
|
738
|
-
self.forward_stack = [] # Clear forward history on new navigation
|
|
739
|
-
self.prompt_search()
|
|
740
|
-
|
|
741
|
-
# Favorites: F, 2
|
|
742
|
-
elif k_char in ['f', 'F', '2']:
|
|
685
|
+
self.selection_idx = 0; self.scroll_offset = 0
|
|
686
|
+
|
|
687
|
+
elif cmd == "ACTIVATE":
|
|
688
|
+
if time.time() - getattr(self, 'last_enter_time', 0) > 0.3:
|
|
689
|
+
self.last_enter_time = time.time()
|
|
690
|
+
self.activate_selection(current_list)
|
|
691
|
+
|
|
692
|
+
elif cmd == "SEARCH":
|
|
693
|
+
self.forward_stack = []; self.prompt_search()
|
|
694
|
+
|
|
695
|
+
elif cmd == "FAVORITES":
|
|
743
696
|
if self.view_stack[-1] != "favorites":
|
|
744
|
-
self.forward_stack = []
|
|
745
|
-
self.view_stack.append("favorites")
|
|
746
|
-
self.selection_idx = 0
|
|
697
|
+
self.forward_stack = []; self.view_stack.append("favorites"); self.selection_idx = 0
|
|
747
698
|
self.status_msg = self.t("favorites_info", DATA_FILE)
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
elif k_char in ['r', 'R', '3']:
|
|
699
|
+
|
|
700
|
+
elif cmd == "HISTORY":
|
|
751
701
|
if self.view_stack[-1] != "history":
|
|
752
|
-
self.forward_stack = []
|
|
753
|
-
self.
|
|
754
|
-
self.view_stack.append("history")
|
|
755
|
-
self.selection_idx = 0
|
|
702
|
+
self.forward_stack = []; self.cached_history = list(self.dm.data['history'])
|
|
703
|
+
self.view_stack.append("history"); self.selection_idx = 0
|
|
756
704
|
self.status_msg = self.t("hist_info")
|
|
757
|
-
|
|
758
|
-
# Main Menu: M, 4
|
|
759
|
-
elif k_char in ['m', 'M', '4']:
|
|
760
|
-
self.forward_stack = [] # Clear forward history
|
|
761
|
-
self.view_stack = ["main"]; self.selection_idx = 0; self.scroll_offset = 0; self.status_msg = ""
|
|
762
|
-
|
|
763
|
-
# Play/Pause: Space
|
|
764
|
-
elif k_char == ' ':
|
|
765
|
-
self.player.toggle_pause()
|
|
766
|
-
|
|
767
|
-
# Volume: 9/0 or [/] or -/+
|
|
768
|
-
elif k_char in ['-','_']:
|
|
769
|
-
self.player.change_volume(-5)
|
|
770
|
-
self.status_msg = "Volume -5"
|
|
771
|
-
elif k_char in ['+','=']:
|
|
772
|
-
self.player.change_volume(5)
|
|
773
|
-
self.status_msg = "Volume +5"
|
|
774
|
-
|
|
775
|
-
# Seek: ,/. (10s), </> (30s)
|
|
776
|
-
elif k_char == ',':
|
|
777
|
-
self.player.seek(-10)
|
|
778
|
-
elif k_char == '.':
|
|
779
|
-
self.player.seek(10)
|
|
780
|
-
elif k_char == '<':
|
|
781
|
-
self.player.seek(-30)
|
|
782
|
-
self.status_msg = "Rewind 30s"
|
|
783
|
-
elif k_char == '>':
|
|
784
|
-
self.player.seek(30)
|
|
785
|
-
self.status_msg = "Forward 30s"
|
|
786
|
-
|
|
787
|
-
elif key == 27:
|
|
788
|
-
self.stop_on_exit = False
|
|
789
|
-
self.running = False
|
|
790
|
-
|
|
791
|
-
# Share Track (F9): Real-time Publish
|
|
792
|
-
elif key == curses.KEY_F9:
|
|
793
|
-
if current_list and 0 <= self.selection_idx < len(current_list):
|
|
794
|
-
target_item = current_list[self.selection_idx]
|
|
795
|
-
url = target_item.get('url')
|
|
796
|
-
title = target_item.get('title', 'Unknown Title')
|
|
797
|
-
|
|
798
|
-
if url:
|
|
799
|
-
# If it's US, try to re-fetch country info one more time (maybe misdetected)
|
|
800
|
-
if self.dm.get_country() == 'US':
|
|
801
|
-
threading.Thread(target=self.dm.fetch_country, daemon=True).start()
|
|
802
|
-
|
|
803
|
-
# Dedup Check: Using a time-based cooldown (e.g. 5 seconds) for same URL
|
|
804
|
-
last_sent_time = self.sent_history.get(url, 0)
|
|
805
|
-
if time.time() - last_sent_time < 5:
|
|
806
|
-
self.status_msg = "⚠️ Already Shared Recently!"
|
|
807
|
-
else:
|
|
808
|
-
try:
|
|
809
|
-
# Send to Serverless Proxy (Secure)
|
|
810
|
-
payload = {
|
|
811
|
-
"title": title,
|
|
812
|
-
"url": url,
|
|
813
|
-
"duration": target_item.get('duration', '--:--'),
|
|
814
|
-
"country": self.dm.get_country(),
|
|
815
|
-
"timestamp": time.time()
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
# v1.9.9 Security Update: Use centralized API with Auth Header
|
|
819
|
-
# v2.0.0 Threading for Smoothness
|
|
820
|
-
def send_share_async(payload, headers, url_to_share, title_to_share):
|
|
821
|
-
try:
|
|
822
|
-
resp = requests.post(
|
|
823
|
-
self.share_api_url,
|
|
824
|
-
json=payload,
|
|
825
|
-
headers=headers,
|
|
826
|
-
timeout=3
|
|
827
|
-
)
|
|
828
|
-
if resp.status_code == 200:
|
|
829
|
-
self.sent_history[url_to_share] = time.time()
|
|
830
|
-
safe_t = self.truncate(title_to_share, 50)
|
|
831
|
-
self.status_msg = f"🚀 Shared: {safe_t}..."
|
|
832
|
-
else:
|
|
833
|
-
self.status_msg = f"❌ Share Error: {resp.status_code}"
|
|
834
|
-
except:
|
|
835
|
-
self.status_msg = "❌ Network Error (API)"
|
|
836
|
-
|
|
837
|
-
headers = {
|
|
838
|
-
"Content-Type": "application/json",
|
|
839
|
-
"x-mytunes-secret": "mytunes-v1-secret-8822"
|
|
840
|
-
}
|
|
841
|
-
threading.Thread(target=send_share_async, args=(payload, headers, url, title), daemon=True).start()
|
|
842
|
-
|
|
843
|
-
except Exception as e:
|
|
844
|
-
self.status_msg = f"❌ Share Failed: {str(e)}"
|
|
845
705
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
706
|
+
elif cmd == "MAIN_MENU":
|
|
707
|
+
self.forward_stack = []; self.view_stack = ["main"]; self.selection_idx = 0; self.scroll_offset = 0; self.status_msg = ""
|
|
708
|
+
|
|
709
|
+
elif cmd == "TOGGLE_PAUSE": self.player.toggle_pause()
|
|
710
|
+
elif cmd == "VOL_DOWN": self.player.change_volume(-5); self.status_msg = "Volume -5"
|
|
711
|
+
elif cmd == "VOL_UP": self.player.change_volume(5); self.status_msg = "Volume +5"
|
|
712
|
+
elif cmd == "SEEK_BACK_10": self.player.seek(-10)
|
|
713
|
+
elif cmd == "SEEK_FWD_10": self.player.seek(10)
|
|
714
|
+
elif cmd == "SEEK_BACK_30": self.player.seek(-30); self.status_msg = "Rewind 30s"
|
|
715
|
+
elif cmd == "SEEK_FWD_30": self.player.seek(30); self.status_msg = "Forward 30s"
|
|
716
|
+
|
|
717
|
+
elif cmd == "TOGGLE_FAV":
|
|
849
718
|
if current_list and 0 <= self.selection_idx < len(current_list):
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
is_added = self.dm.toggle_favorite(target_item)
|
|
719
|
+
target = current_list[self.selection_idx]
|
|
720
|
+
if "url" in target:
|
|
721
|
+
is_added = self.dm.toggle_favorite(target)
|
|
854
722
|
self.status_msg = self.t("fav_added") if is_added else self.t("fav_removed")
|
|
855
723
|
|
|
856
|
-
|
|
857
|
-
|
|
724
|
+
elif cmd == "DELETE":
|
|
725
|
+
self.handle_deletion(current_list)
|
|
726
|
+
|
|
727
|
+
elif cmd == "OPEN_BROWSER":
|
|
858
728
|
if current_list and 0 <= self.selection_idx < len(current_list):
|
|
859
|
-
|
|
860
|
-
url
|
|
861
|
-
if url:
|
|
862
|
-
if self.is_remote():
|
|
863
|
-
self.show_copy_dialog("YouTube", url)
|
|
864
|
-
else:
|
|
865
|
-
self.open_browser(url)
|
|
729
|
+
url = current_list[self.selection_idx].get('url')
|
|
730
|
+
if url: (self.show_copy_dialog("Media", url) if self.is_remote() else self.open_browser(url))
|
|
866
731
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
self.show_copy_dialog("MyTunes Home", homepage_url)
|
|
872
|
-
return
|
|
732
|
+
elif cmd in ["OPEN_HOME"]:
|
|
733
|
+
url = "https://mytunes-pro.com"
|
|
734
|
+
if self.is_remote(): self.show_copy_dialog("MyTunes Home", url)
|
|
735
|
+
else: self.open_browser(url, app_mode=False)
|
|
873
736
|
|
|
874
|
-
|
|
737
|
+
elif cmd == "OPEN_PARTNER":
|
|
738
|
+
self.open_browser("https://postgresql.co.kr")
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
elif cmd == "RESIZE":
|
|
742
|
+
self.stdscr.clear()
|
|
743
|
+
self.stdscr.refresh()
|
|
744
|
+
|
|
745
|
+
elif cmd == "EXIT_BKG":
|
|
746
|
+
self.stop_on_exit = False; self.running = False
|
|
747
|
+
|
|
748
|
+
def handle_deletion(self, current_list):
|
|
749
|
+
"""Sub-logic for DELETE command to keep dispatcher clean."""
|
|
750
|
+
if not current_list or not (0 <= self.selection_idx < len(current_list)): return
|
|
751
|
+
|
|
752
|
+
view = self.view_stack[-1]
|
|
753
|
+
success = False
|
|
754
|
+
if view == "favorites":
|
|
755
|
+
success = self.dm.remove_favorite_by_index(self.selection_idx)
|
|
756
|
+
if success: self.status_msg = "🗑️ Deleted from Favorites"
|
|
757
|
+
elif view == "history":
|
|
758
|
+
success = self.dm.remove_history_by_index(self.selection_idx)
|
|
759
|
+
if success: self.cached_history = list(self.dm.data['history']); self.status_msg = "🗑️ Deleted from History"
|
|
760
|
+
elif view == "search":
|
|
761
|
+
if self.current_search_query is None:
|
|
762
|
+
success = self.dm.remove_search_history_by_index(self.selection_idx)
|
|
763
|
+
if success: self.search_results = self.dm.get_search_history(); self.status_msg = "🗑️ Deleted from Search History"
|
|
764
|
+
else:
|
|
765
|
+
try: self.search_results.pop(self.selection_idx); success = True; self.status_msg = "Removed from list"
|
|
766
|
+
except: pass
|
|
767
|
+
if success:
|
|
768
|
+
self.selection_idx = max(0, min(self.selection_idx, len(self.get_current_list()) - 1))
|
|
875
769
|
|
|
876
|
-
# Delete Item: DEL, d
|
|
877
|
-
elif key == curses.KEY_DC or k_char in ['d']:
|
|
878
|
-
if current_list and 0 <= self.selection_idx < len(current_list):
|
|
879
|
-
view = self.view_stack[-1]
|
|
880
|
-
success = False
|
|
881
|
-
|
|
882
|
-
if view == "favorites":
|
|
883
|
-
success = self.dm.remove_favorite_by_index(self.selection_idx)
|
|
884
|
-
if success: self.status_msg = "🗑️ Deleted from Favorites"
|
|
885
|
-
|
|
886
|
-
elif view == "history":
|
|
887
|
-
success = self.dm.remove_history_by_index(self.selection_idx)
|
|
888
|
-
if success:
|
|
889
|
-
self.cached_history = list(self.dm.data['history']) # Refresh view
|
|
890
|
-
self.status_msg = "🗑️ Deleted from History"
|
|
891
|
-
|
|
892
|
-
elif view == "search":
|
|
893
|
-
# If current_search_query is None, we are viewing Search History
|
|
894
|
-
if self.current_search_query is None:
|
|
895
|
-
success = self.dm.remove_search_history_by_index(self.selection_idx)
|
|
896
|
-
if success:
|
|
897
|
-
self.search_results = self.dm.get_search_history() # Refresh
|
|
898
|
-
self.status_msg = "🗑️ Deleted from Search History"
|
|
899
|
-
else:
|
|
900
|
-
# Ephemeral removal from result list
|
|
901
|
-
try:
|
|
902
|
-
self.search_results.pop(self.selection_idx)
|
|
903
|
-
self.status_msg = "Start new search"
|
|
904
|
-
success = True
|
|
905
|
-
except: pass
|
|
906
|
-
|
|
907
|
-
if success:
|
|
908
|
-
# Adjust selection index if out of bounds
|
|
909
|
-
# If list became empty, idx will be 0 but len is 0.
|
|
910
|
-
# We just need to ensure we don't crash next draw.
|
|
911
|
-
# The draw logic (get_current_list) handles empty lists safely.
|
|
912
|
-
if self.selection_idx >= len(self.get_current_list()):
|
|
913
|
-
self.selection_idx = max(0, len(self.get_current_list()) - 1)
|
|
914
770
|
|
|
915
771
|
|
|
916
772
|
|
|
@@ -947,20 +803,26 @@ class MyTunesApp:
|
|
|
947
803
|
curses.flushinp()
|
|
948
804
|
|
|
949
805
|
while True:
|
|
950
|
-
|
|
951
|
-
|
|
806
|
+
try:
|
|
807
|
+
k = win.get_wch()
|
|
808
|
+
except curses.error:
|
|
809
|
+
continue
|
|
952
810
|
|
|
953
811
|
# ESC -> Background Play (Exit app)
|
|
954
|
-
if k == 27:
|
|
812
|
+
if k == 27 or k == '\x1b':
|
|
955
813
|
self.stop_on_exit = False
|
|
956
814
|
self.running = False
|
|
957
|
-
res = False
|
|
815
|
+
res = False
|
|
958
816
|
break
|
|
959
817
|
|
|
960
|
-
|
|
818
|
+
# Enter / Space -> Resume
|
|
819
|
+
if k in [10, 13, curses.KEY_ENTER, '\n', '\r', ' ']:
|
|
961
820
|
res = True
|
|
962
821
|
break
|
|
963
|
-
|
|
822
|
+
|
|
823
|
+
# 0 / R -> Restart
|
|
824
|
+
k_char = str(k).lower() if isinstance(k, str) else ""
|
|
825
|
+
if k_char in ['0', 'r']:
|
|
964
826
|
res = False
|
|
965
827
|
break
|
|
966
828
|
|
|
@@ -1022,6 +884,12 @@ class MyTunesApp:
|
|
|
1022
884
|
else:
|
|
1023
885
|
# Linux or others
|
|
1024
886
|
subprocess.Popen(['xdg-open', url], **popen_kwargs)
|
|
887
|
+
|
|
888
|
+
# Feedback logic: Success message then auto-clear
|
|
889
|
+
self.status_msg = "✅ Browser Launched! (Check Browser)"
|
|
890
|
+
time.sleep(2.5)
|
|
891
|
+
if "Launched!" in self.status_msg:
|
|
892
|
+
self.status_msg = ""
|
|
1025
893
|
except Exception as e:
|
|
1026
894
|
# Log error silently to TUI status
|
|
1027
895
|
self.status_msg = f"❌ Browser Error: {str(e)[:20]}"
|
|
@@ -1075,8 +943,12 @@ class MyTunesApp:
|
|
|
1075
943
|
|
|
1076
944
|
# Wait for key
|
|
1077
945
|
while True:
|
|
1078
|
-
|
|
1079
|
-
|
|
946
|
+
try:
|
|
947
|
+
k = win.get_wch()
|
|
948
|
+
except curses.error:
|
|
949
|
+
continue
|
|
950
|
+
|
|
951
|
+
if k in [10, 13, curses.KEY_ENTER, 27, '\n', '\r', '\x1b', ' ']:
|
|
1080
952
|
break
|
|
1081
953
|
except: pass
|
|
1082
954
|
finally:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mytunes-pro
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.4
|
|
4
4
|
Summary: A lightweight, keyboard-centric terminal player for streaming YouTube music.
|
|
5
5
|
Author-email: loxo <loxo5432@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/postgresql-co-kr/mytunes
|
|
@@ -17,18 +17,23 @@ Requires-Dist: yt-dlp
|
|
|
17
17
|
Requires-Dist: pusher
|
|
18
18
|
Dynamic: license-file
|
|
19
19
|
|
|
20
|
-
# 🎵 MyTunes Pro
|
|
20
|
+
# 🎵 MyTunes Pro - Professional TUI Edition v2.0.4
|
|
21
21
|
|
|
22
|
-
## 🚀
|
|
22
|
+
## 🚀 Terminal-based Media Workflow Experiment v2.0.4
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
> [!IMPORTANT]
|
|
25
|
+
> **Legal Disclaimer:** This project is a personal, non-commercial research experiment for developer education.
|
|
26
|
+
> It does not host, provide, or distribute any media content.
|
|
27
|
+
> All media sources are independently accessed and configured by the user.
|
|
28
|
+
> Users are solely responsible for ensuring that their usage complies with the terms of service of any third-party platforms accessed via this tool.
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
MyTunes Pro is a developer-focused **CLI Media Tool** for experimenting with terminal-based media workflows.
|
|
31
|
+
It utilizes the Python `curses` library to provide a structured TUI (Terminal User Interface) for handling media URLs,
|
|
32
|
+
leveraging the `mpv` engine for local media processing and playback.
|
|
33
|
+
|
|
34
|
+
> **💡 Project Note**
|
|
35
|
+
> This tool was designed for personal research into how terminal users can interact with media sources without interrupting their developer workflow.
|
|
36
|
+
> It explores the integration between local CLI environments (like Headless Debian Servers) and external media handling utilities.
|
|
32
37
|
|
|
33
38
|

|
|
34
39
|

|
|
@@ -41,27 +46,24 @@ Python `curses` 라이브러리를 사용하여 터미널 환경에서도 **GUI
|
|
|
41
46
|
|
|
42
47
|
---
|
|
43
48
|
|
|
44
|
-
## ✨
|
|
49
|
+
## ✨ Core Features
|
|
45
50
|
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
- **라이브 (F8)**: 전 세계 유저들과 함께 듣는 **실시간 음악 대시보드** (전용 팝업).
|
|
53
|
-
- **공유 (F9)**: 내가 듣는 곡을 **라이브 스테이션에 즉시 송출**하여 함께 즐깁니다.
|
|
54
|
-
- **비주얼**: 현대적인 심볼 아이콘(⌕, ★, ◷)과 깔끔한 디자인.
|
|
51
|
+
- **Media Handling**: Support for loading and processing media URLs using external extraction tools.
|
|
52
|
+
- **TUI Workflow**: Efficient, low-latency interface built on the `curses` library.
|
|
53
|
+
- **Workflow Persistence**: Handles sequential media loading and state restoration.
|
|
54
|
+
- **Terminal Optimization**: Performance-focused design that prioritizes keyboard-driven interactions.
|
|
55
|
+
- **Smart Management**: Optional user-configured collections, interaction history, and metadata handling.
|
|
56
|
+
- **External Integration**: Capabilities to load media links into external viewer/player environments.
|
|
55
57
|
|
|
56
58
|
---
|
|
57
59
|
|
|
58
|
-
## 💻
|
|
60
|
+
## 💻 Environment & Integration
|
|
59
61
|
|
|
60
|
-
**MyTunes Pro
|
|
62
|
+
**MyTunes Pro** is a CLI-based tool. It can integrate with externally installed media processing tools.
|
|
61
63
|
|
|
62
|
-
- **
|
|
63
|
-
- **Linux**:
|
|
64
|
-
- **Windows**: **WSL(Windows Subsystem for Linux)
|
|
64
|
+
- **External Tools**: This project can interface with user-installed utilities like `mpv` and media extraction tools. No third-party tools are bundled with this software.
|
|
65
|
+
- **macOS/Linux**: Native terminal support.
|
|
66
|
+
- **Windows**: Recommended to use with **WSL (Windows Subsystem for Linux)**.
|
|
65
67
|
|
|
66
68
|
---
|
|
67
69
|
|
|
@@ -182,8 +184,6 @@ Windows 환경에서 한글 검색이 안 되거나 설치가 어려운 분들
|
|
|
182
184
|
| **`+`** | **볼륨 UP** | 볼륨 +5% (단축키 `=`와 동일) |
|
|
183
185
|
| **`-`** | **볼륨 DOWN** | 볼륨 -5% (단축키 `_`와 동일) |
|
|
184
186
|
| **`F7`** | **유튜브 열기** | 현재 곡을 브라우저에서 보기 |
|
|
185
|
-
| **`F8`** | **라이브 (Live)** | **실시간 음악 대시보드 열기** (전용 팝업창) |
|
|
186
|
-
| **`F9`** | **공유 (Share)** | **현재 곡을 라이브 스테이션에 즉시 공유** |
|
|
187
187
|
| **`6`** | **뒤로가기** | 이전 화면으로 이동 (단축키 `Q`, `h`와 동일) |
|
|
188
188
|
| **`L`** | **앞으로** | 이전 화면에서 앞화면으로 다시 이동 (`Right Arrow`) |
|
|
189
189
|
| **`ESC`** | **배경재생** | **음악 끄지 않고 나가기** (백그라운드 재생) |
|
|
@@ -210,100 +210,48 @@ Windows 환경에서 한글 검색이 안 되거나 설치가 어려운 분들
|
|
|
210
210
|
---
|
|
211
211
|
---
|
|
212
212
|
|
|
213
|
-
# 🎵 MyTunes Pro (English)
|
|
214
|
-
|
|
215
|
-
**Modern CLI YouTube Music Player (v2.0.2)**
|
|
216
|
-
A lightweight, keyboard-centric terminal player for streaming YouTube music.
|
|
217
|
-
|
|
218
213
|
---
|
|
219
214
|
|
|
220
|
-
|
|
215
|
+
# 🎵 MyTunes Pro (Experimental Media Tool - KR)
|
|
221
216
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
- **macOS**: Native Terminal support. Python 3.9+ required.
|
|
225
|
-
- **Linux**: Supports all distributions (Ubuntu, Debian, etc.).
|
|
226
|
-
- **Windows**: Requires **WSL (Windows Subsystem for Linux)**.
|
|
227
|
-
|
|
228
|
-
---
|
|
217
|
+
## 🚀 터미널 기반 미디어 워크플로우 실험 v2.0.3
|
|
229
218
|
|
|
230
|
-
|
|
219
|
+
> [!IMPORTANT]
|
|
220
|
+
> **법적 면책 고지:** 본 프로젝트는 개발자 교육 및 연구를 목적으로 하는 개인적, 비상업적 실험입니다.
|
|
221
|
+
> 본 소프트웨어는 어떠한 미디어 콘텐츠도 직접 호스팅하거나 배포하지 않습니다.
|
|
222
|
+
> 모든 미디어 소스는 사용자의 로컬 환경에서 직접 구성되고 접근되며, 사용자는 외부 플랫폼의 이용 약관을 준수할 책임이 있습니다.
|
|
231
223
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
```bash
|
|
236
|
-
pipx install mytunes-pro
|
|
237
|
-
pipx ensurepath
|
|
238
|
-
source ~/.zshrc # or source ~/.bashrc to apply changes immediately
|
|
239
|
-
```
|
|
224
|
+
MyTunes Pro는 개발자를 위해 설계된 **CLI 미디어 실험 도구**입니다.
|
|
225
|
+
Python `curses` 라이브러리를 통해 터미널 환경에서 미디어 URL을 로드하고 관리하며,
|
|
226
|
+
사용자가 설치한 `mpv` 등의 외부 도구와 연동하여 미디어 워크플로우를 테스트할 수 있습니다.
|
|
240
227
|
|
|
241
|
-
|
|
242
|
-
```bash
|
|
243
|
-
pip install mytunes-pro --break-system-packages
|
|
244
|
-
```
|
|
228
|
+
## ✨ 주요 특징
|
|
245
229
|
|
|
246
|
-
|
|
230
|
+
- **미디어 핸들링**: 외부 추출 도구를 사용한 미디어 URL 로드 및 처리 지원.
|
|
231
|
+
- **TUI 워크플로우**: `curses` 라이브러리 기반의 효율적인 터미널 인터페이스.
|
|
232
|
+
- **작업 유지**: 순차적 미디어 로딩 및 마지막 작업 상태 복원 기능.
|
|
233
|
+
- **환경 연동**: 사용자에 의해 구성된 외부 미디어 도구와의 연동 지원. (본 소프트웨어는 외부 도구를 포함하여 배포하지 않습니다.)
|
|
247
234
|
|
|
248
235
|
---
|
|
249
236
|
|
|
250
|
-
##
|
|
251
|
-
|
|
252
|
-
### macOS (Homebrew)
|
|
253
|
-
```bash
|
|
254
|
-
brew install mpv python3 pipx
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
### Linux (Ubuntu/Debian)
|
|
258
|
-
```bash
|
|
259
|
-
sudo apt update
|
|
260
|
-
sudo apt install mpv python3 python3-pip pipx python3-venv -y
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
### Windows (Beginner's WSL Guide)
|
|
264
|
-
|
|
265
|
-
1. **Install WSL**:
|
|
266
|
-
```powershell
|
|
267
|
-
wsl --install -d Debian
|
|
268
|
-
```
|
|
269
|
-
**Restart your computer** after installation.
|
|
270
|
-
|
|
271
|
-
2. **Install Core Tools**:
|
|
272
|
-
```bash
|
|
273
|
-
sudo apt update && sudo apt install mpv python3-pip pipx -y
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
4. **Install MyTunes**:
|
|
277
|
-
```bash
|
|
278
|
-
pipx install mytunes-pro
|
|
279
|
-
pipx ensurepath
|
|
280
|
-
source ~/.bashrc
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
---
|
|
237
|
+
## 🔄 Changelog
|
|
284
238
|
|
|
285
|
-
|
|
239
|
+
### v2.0.4 (2026-02-01)
|
|
240
|
+
- **Legal Polish**: Comprehensive scrubbing of brand identifiers and service-oriented terminology across the ecosystem.
|
|
241
|
+
- **Localization**: Fully localized Korean landing page and technical experiment descriptions.
|
|
242
|
+
- **Educational Focus**: Added explicit project disclaimers to all web footers.
|
|
243
|
+
- **Project Scope**: Solidified positioning as a "Media Handling Experiment" rather than a music player.
|
|
286
244
|
|
|
287
|
-
|
|
288
|
-
| :--- | :--- | :--- |
|
|
289
|
-
| **`1`** | **Search** | Open search bar (Same as `S`) |
|
|
290
|
-
| **`2`** | **Favs** | View favorites list (Same as `F`) |
|
|
291
|
-
| **`3`** | **Hist** | View history (Same as `R`) |
|
|
292
|
-
| **`4`** | **Main** | Go to Main Menu (Same as `M`) |
|
|
293
|
-
| **`5`** | **Add/Del** | Toggle Favorite (Same as `A`) |
|
|
294
|
-
| **`+`** | **Vol Up** | Volume +5% (Same as `=`) |
|
|
295
|
-
| **`-`** | **Vol Down** | Volume -5% (Same as `_`) |
|
|
296
|
-
| **`6`** | **Back** | Go back (Same as `Q`, `h`) |
|
|
297
|
-
| **`L`** | **Forward** | Go forward (`Right Arrow`) |
|
|
298
|
-
| **`ESC`** | **Bg Play** | **Exit app but keep music playing** |
|
|
245
|
+
### v2.0.3 (Input Handling & Unicode Stability)
|
|
299
246
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
247
|
+
- **Input Logic**: Replaced legacy `getch()` with `get_wch()` in all UI dialogs (`ask_resume`, `show_copy_dialog`) for robust wide-character and Unicode support.
|
|
248
|
+
- **Architecture**: Refactored the input handling system into a modular, command-based architecture (v2.0.3).
|
|
249
|
+
- **Decoupling**: Separated input collection (`get_next_event`), event normalization, and command execution.
|
|
250
|
+
- **Improved ESC Handling**: Enhanced detection of ESC and multi-byte sequences (including Option+Backspace) for smoother navigation.
|
|
303
251
|
|
|
304
252
|
### v2.0.2 (Stability & Browser Optimization)
|
|
305
253
|
|
|
306
|
-
- **Browser Launch**: Switched to fully decoupled `subprocess.Popen` logic for browser opening. This eliminates occasional TUI freezes when launching
|
|
254
|
+
- **Browser Launch**: Switched to fully decoupled `subprocess.Popen` logic for browser opening. This eliminates occasional TUI freezes when launching Media Links (F7) or Dashboard (F8) by bypassing `webbrowser` library limitations.
|
|
307
255
|
- **App Mode Restore**: Fixed and improved Chrome/Brave App Mode (Popup) for the Live Station on macOS.
|
|
308
256
|
- **Improved Remote Detection**: Refined SSH/WSL detection to ensure local browser features are correctly enabled where possible.
|
|
309
257
|
|
|
@@ -318,7 +266,6 @@ sudo apt install mpv python3 python3-pip pipx python3-venv -y
|
|
|
318
266
|
|
|
319
267
|
- **Domain Migration**: Updated all branding and internal links to support `mytunes-pro.com`.
|
|
320
268
|
- **Realtime Stability**: Fixed critical state-management bugs in the live dashboard that caused list clearing and duplicated track entries.
|
|
321
|
-
- **Pusher Channel Synchronization**: Unified communication across the transition to ensure zero-latency sharing on the new `mytunes-pro` channel.
|
|
322
269
|
- **Improved Empty State**: Redesigned the "SIGNAL LOST" screen into a more descriptive "READY TO RECEIVE" interface for better UX.
|
|
323
270
|
|
|
324
271
|
### v1.9.8 (Realtime Stabilization)
|
|
@@ -326,7 +273,7 @@ sudo apt install mpv python3 python3-pip pipx python3-venv -y
|
|
|
326
273
|
- **UI Refinement**: Implemented in-list "Now Playing" sticky behavior with auto-scroll synchronization for a seamless browsing experience.
|
|
327
274
|
- **Queue System Optimization**: Capped incoming track queue at 200 items with a "200+" notification indicator for high-traffic stability.
|
|
328
275
|
- **Popup UI Consistency**: Unified Live Station popup dimensions to 620x900 across Web and TUI.
|
|
329
|
-
- **Improved
|
|
276
|
+
- **Improved Media Playback**: Optimized the media player hook to resolve initialization race conditions and syntax edge cases.
|
|
330
277
|
|
|
331
278
|
### v1.9.7 (Analytics)
|
|
332
279
|
|
|
@@ -386,7 +333,7 @@ sudo apt install mpv python3 python3-pip pipx python3-venv -y
|
|
|
386
333
|
### v1.8.4
|
|
387
334
|
|
|
388
335
|
- **Python Crash Fix (WSL)**: Eliminated premature termination by implementing `start_new_session=True` for browser launches, isolating them from the TUI process group.
|
|
389
|
-
- **Hybrid Browser Strategy**: Switched to the standard `webbrowser` library for F7 (
|
|
336
|
+
- **Hybrid Browser Strategy**: Switched to the standard `webbrowser` library for F7 (Media links) for maximum internal stability.
|
|
390
337
|
- **Global Error Protection**: Wrapped the main application loop in an exception guard to catch and log transient OS errors without crashing the entire app.
|
|
391
338
|
- **Refined Process Cleanup**: Specialized the `pkill` logic to prevent accidental self-termination while maintaining reliable MPV management.
|
|
392
339
|
|
|
@@ -433,7 +380,7 @@ sudo apt install mpv python3 python3-pip pipx python3-venv -y
|
|
|
433
380
|
### v1.7.5
|
|
434
381
|
|
|
435
382
|
- **WSL Integration**: Fully optimized browser launch from WSL by utilizing `cmd.exe` to trigger native Windows browsers.
|
|
436
|
-
- **F7 Windows Resolve**: Fixed an issue where
|
|
383
|
+
- **F7 Windows Resolve**: Fixed an issue where Media links (F7) wouldn't open in WSL environments.
|
|
437
384
|
- **F8 App Mode (WSL/Win)**: Enhanced flags to ensure "App Mode" (no address bar) works consistently even when launched from WSL.
|
|
438
385
|
|
|
439
386
|
### v1.7.4
|
|
@@ -443,7 +390,7 @@ sudo apt install mpv python3 python3-pip pipx python3-venv -y
|
|
|
443
390
|
|
|
444
391
|
### v1.7.3
|
|
445
392
|
|
|
446
|
-
- **Windows Fixes**: Resolved issue where F7 (
|
|
393
|
+
- **Windows Fixes**: Resolved issue where F7 (Media) failed to open browsers on Windows by implementing `os.startfile` logic.
|
|
447
394
|
- **F8 Initialization**: Improved Live Station (F8) window sizing on Windows by forcing a clean session state.
|
|
448
395
|
- **Robustness**: Enhanced cross-platform browser redirection logic to ensure consistent behavior.
|
|
449
396
|
|
|
@@ -463,7 +410,6 @@ sudo apt install mpv python3 python3-pip pipx python3-venv -y
|
|
|
463
410
|
### v1.6.0
|
|
464
411
|
|
|
465
412
|
- **Global Version Synchronization**: Synchronized version 1.6.0 across CLI, README, and Web interface.
|
|
466
|
-
- **Dependency Fix**: Ensured `pusher` dependency is correctly included for real-time features.
|
|
467
413
|
|
|
468
414
|
### v1.5.6
|
|
469
415
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mytunes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
mytunes/app.py,sha256=iu9LN4WtJrSQLSMreBMZqnXYotm_9Bk5eoQVTxk6dCo,57463
|
|
3
|
+
mytunes_pro-2.0.4.dist-info/licenses/LICENSE,sha256=lOrP0EIjxcgJia__W3f3PVDZkRd2oRzFkyH2g3LRRCg,1063
|
|
4
|
+
mytunes_pro-2.0.4.dist-info/METADATA,sha256=H_ujUXf1ofYs1seveke-gV_ivc-VAPYU9kKJgpaCpdE,22542
|
|
5
|
+
mytunes_pro-2.0.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
6
|
+
mytunes_pro-2.0.4.dist-info/entry_points.txt,sha256=6-MsC13nIgzLvrREaGotc32FgxHx_Iuu1z2qCzJs1_4,65
|
|
7
|
+
mytunes_pro-2.0.4.dist-info/top_level.txt,sha256=KWzdFyNNG_sO7GT83-sN5fYArP4_DL5I8HYIwgazXyY,8
|
|
8
|
+
mytunes_pro-2.0.4.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
mytunes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
mytunes/app.py,sha256=KlwbzDtBQ2lNg9KC4jhBXi5A-L4j5WkiJ67AatiGo9w,63780
|
|
3
|
-
mytunes_pro-2.0.2.dist-info/licenses/LICENSE,sha256=lOrP0EIjxcgJia__W3f3PVDZkRd2oRzFkyH2g3LRRCg,1063
|
|
4
|
-
mytunes_pro-2.0.2.dist-info/METADATA,sha256=u8SH6WZ5WKRf3ARn8M3TkPad0h5A91Yz3sdiHzTmlck,22830
|
|
5
|
-
mytunes_pro-2.0.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
6
|
-
mytunes_pro-2.0.2.dist-info/entry_points.txt,sha256=6-MsC13nIgzLvrREaGotc32FgxHx_Iuu1z2qCzJs1_4,65
|
|
7
|
-
mytunes_pro-2.0.2.dist-info/top_level.txt,sha256=KWzdFyNNG_sO7GT83-sN5fYArP4_DL5I8HYIwgazXyY,8
|
|
8
|
-
mytunes_pro-2.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|