mytunes-pro 1.9.9__py3-none-any.whl → 2.0.1__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 +235 -215
- {mytunes_pro-1.9.9.dist-info → mytunes_pro-2.0.1.dist-info}/METADATA +20 -8
- mytunes_pro-2.0.1.dist-info/RECORD +8 -0
- mytunes_pro-1.9.9.dist-info/RECORD +0 -8
- {mytunes_pro-1.9.9.dist-info → mytunes_pro-2.0.1.dist-info}/WHEEL +0 -0
- {mytunes_pro-1.9.9.dist-info → mytunes_pro-2.0.1.dist-info}/entry_points.txt +0 -0
- {mytunes_pro-1.9.9.dist-info → mytunes_pro-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {mytunes_pro-1.9.9.dist-info → mytunes_pro-2.0.1.dist-info}/top_level.txt +0 -0
mytunes/app.py
CHANGED
|
@@ -18,12 +18,8 @@ import locale
|
|
|
18
18
|
import signal
|
|
19
19
|
import warnings
|
|
20
20
|
import webbrowser
|
|
21
|
-
# Suppress urllib3 warning about LibreSSL compatibility
|
|
22
|
-
warnings.filterwarnings("ignore", message=".*urllib3 v2 only supports OpenSSL 1.1.1+.*")
|
|
23
|
-
import webbrowser
|
|
24
21
|
import tempfile
|
|
25
22
|
import shutil
|
|
26
|
-
import pusher
|
|
27
23
|
import requests
|
|
28
24
|
|
|
29
25
|
|
|
@@ -36,7 +32,7 @@ MPV_SOCKET = "/tmp/mpv_socket"
|
|
|
36
32
|
LOG_FILE = "/tmp/mytunes_mpv.log"
|
|
37
33
|
PID_FILE = "/tmp/mytunes_mpv.pid"
|
|
38
34
|
APP_NAME = "MyTunes Pro"
|
|
39
|
-
APP_VERSION = "
|
|
35
|
+
APP_VERSION = "2.0.1"
|
|
40
36
|
|
|
41
37
|
# === [Strings & Localization] ===
|
|
42
38
|
STRINGS = {
|
|
@@ -56,7 +52,7 @@ STRINGS = {
|
|
|
56
52
|
"fav_added": "★ 즐겨찾기에 추가됨",
|
|
57
53
|
"fav_removed": "☆ 즐겨찾기 해제됨",
|
|
58
54
|
"header_r1": "[S/1]검색 [F/2]즐겨찾기 [R/3]기록 [M/4]메인 [A/5]즐겨찾기추가 [Q/6]뒤로",
|
|
59
|
-
"header_r2": "[F7]유튜브 [F8]라이브 [F9]라이브공유 [SPC]Play/Stop [+/-]볼륨 [<>]빨리감기",
|
|
55
|
+
"header_r2": "[F7]유튜브 [F8]라이브 [F9]라이브공유 [SPC]Play/Stop [+/-]볼륨 [<>]빨리감기 [D/Del]삭제",
|
|
60
56
|
"help_guide": "[j/k]이동 [En]선택 [h/q]뒤로 [S/1]검색 [F/2]즐겨찾기 [R/3]기록 [M/4]메인 [F7]유튜브 [F8]라이브 [F9]라이브공유",
|
|
61
57
|
"menu_main": "☰ 메인 메뉴",
|
|
62
58
|
"menu_search_results": "⌕ YouTube 음악 검색",
|
|
@@ -85,7 +81,7 @@ STRINGS = {
|
|
|
85
81
|
"fav_added": "★ Added to Favorites",
|
|
86
82
|
"fav_removed": "☆ Removed from Favorites",
|
|
87
83
|
"header_r1": "[S/1]Srch [F/2]Favs [R/3]Hist [M/4]Main [A/5]AddFav [Q/6]Back",
|
|
88
|
-
"header_r2": "[F7]YT [F8]Live [F9]LiveShare [SPC]Play/Stop [+/-]Vol [<>]Seek",
|
|
84
|
+
"header_r2": "[F7]YT [F8]Live [F9]LiveShare [SPC]Play/Stop [+/-]Vol [<>]Seek [D/Del]Del",
|
|
89
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 [F8]Live [F9]Share",
|
|
90
86
|
"menu_main": "☰ Main Menu",
|
|
91
87
|
"menu_search_results": "⌕ Search YouTube Music",
|
|
@@ -100,11 +96,11 @@ STRINGS = {
|
|
|
100
96
|
}
|
|
101
97
|
}
|
|
102
98
|
|
|
103
|
-
# === [Data Management] ===
|
|
104
99
|
class DataManager:
|
|
105
100
|
def __init__(self):
|
|
106
101
|
self.data = self.load_data()
|
|
107
102
|
self.favorites_set = {f['url'] for f in self.data.get('favorites', []) if 'url' in f}
|
|
103
|
+
self.lock = threading.Lock()
|
|
108
104
|
|
|
109
105
|
# Auto-fetch country if missing
|
|
110
106
|
if 'country' not in self.data:
|
|
@@ -124,8 +120,11 @@ class DataManager:
|
|
|
124
120
|
return {"history": [], "favorites": [], "language": "ko", "resume": {}, "search_results_history": []}
|
|
125
121
|
|
|
126
122
|
def save_data(self):
|
|
127
|
-
with
|
|
128
|
-
|
|
123
|
+
with self.lock:
|
|
124
|
+
try:
|
|
125
|
+
with open(DATA_FILE, "w", encoding="utf-8") as f:
|
|
126
|
+
json.dump(self.data, f, indent=2, ensure_ascii=False)
|
|
127
|
+
except Exception: pass
|
|
129
128
|
|
|
130
129
|
def get_progress(self, url):
|
|
131
130
|
return self.data.get("resume", {}).get(url, 0)
|
|
@@ -230,16 +229,35 @@ class DataManager:
|
|
|
230
229
|
self.data['search_results_history'] = unique_history[:200]
|
|
231
230
|
self.save_data()
|
|
232
231
|
|
|
232
|
+
def remove_favorite_by_index(self, index):
|
|
233
|
+
if 0 <= index < len(self.data['favorites']):
|
|
234
|
+
item = self.data['favorites'].pop(index)
|
|
235
|
+
if item.get('url') in self.favorites_set:
|
|
236
|
+
self.favorites_set.remove(item['url'])
|
|
237
|
+
self.save_data()
|
|
238
|
+
return True
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
def remove_history_by_index(self, index):
|
|
242
|
+
if 0 <= index < len(self.data['history']):
|
|
243
|
+
self.data['history'].pop(index)
|
|
244
|
+
self.save_data()
|
|
245
|
+
return True
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
def remove_search_history_by_index(self, index):
|
|
249
|
+
if 0 <= index < len(self.data['search_results_history']):
|
|
250
|
+
self.data['search_results_history'].pop(index)
|
|
251
|
+
self.save_data()
|
|
252
|
+
return True
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
|
|
233
256
|
# === [Player Logic with Advanced IPC] ===
|
|
234
257
|
class Player:
|
|
235
258
|
def __init__(self):
|
|
236
259
|
self.current_proc = None
|
|
237
260
|
self.loading = False
|
|
238
|
-
|
|
239
|
-
self.current_proc = None
|
|
240
|
-
self.loading = False
|
|
241
|
-
|
|
242
|
-
|
|
243
261
|
self.loading_ts = 0
|
|
244
262
|
|
|
245
263
|
# Cleanup pre-existing instance if any
|
|
@@ -352,7 +370,7 @@ class Player:
|
|
|
352
370
|
"""Send raw command list to MPV via JSON IPC."""
|
|
353
371
|
try:
|
|
354
372
|
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
355
|
-
client.settimeout(0.
|
|
373
|
+
client.settimeout(0.5) # Fast timeout (Optimization for Sleep/Wake resilience)
|
|
356
374
|
client.connect(MPV_SOCKET)
|
|
357
375
|
cmd_str = json.dumps({"command": command}) + "\n"
|
|
358
376
|
client.send(cmd_str.encode('utf-8'))
|
|
@@ -410,10 +428,11 @@ class MyTunesApp:
|
|
|
410
428
|
|
|
411
429
|
# Search State
|
|
412
430
|
self.current_search_query = None
|
|
413
|
-
self.search_page = 1
|
|
414
|
-
self.is_loading_more = False
|
|
431
|
+
# self.search_page = 1 # Deprecated: Pagination Removed v2.0.2
|
|
432
|
+
# self.is_loading_more = False # Deprecated
|
|
415
433
|
|
|
416
434
|
# Playback State
|
|
435
|
+
|
|
417
436
|
self.playback_time = 0
|
|
418
437
|
self.playback_duration = 0
|
|
419
438
|
self.is_paused = False
|
|
@@ -436,6 +455,8 @@ class MyTunesApp:
|
|
|
436
455
|
curses.curs_set(0)
|
|
437
456
|
self.stdscr.nodelay(True)
|
|
438
457
|
self.stdscr.timeout(200) # Update loop every 200ms
|
|
458
|
+
self.last_input_time = time.time() # For Idle Detection
|
|
459
|
+
|
|
439
460
|
|
|
440
461
|
# Register Signal for Terminal Disconnect (Window Close)
|
|
441
462
|
try:
|
|
@@ -446,16 +467,9 @@ class MyTunesApp:
|
|
|
446
467
|
curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION)
|
|
447
468
|
print("\033[?1003h") # Enable mouse tracking
|
|
448
469
|
|
|
449
|
-
#
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
app_id='2106370',
|
|
453
|
-
key='44e3d7e4957944c867ec',
|
|
454
|
-
secret='0be8e65a287bbccc7369',
|
|
455
|
-
cluster='ap3',
|
|
456
|
-
ssl=True
|
|
457
|
-
)
|
|
458
|
-
except: self.pusher = None
|
|
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"
|
|
459
473
|
self.sent_history = {}
|
|
460
474
|
|
|
461
475
|
|
|
@@ -530,6 +544,15 @@ class MyTunesApp:
|
|
|
530
544
|
elif self.playback_time > 10:
|
|
531
545
|
self.dm.set_progress(self.current_track['url'], self.playback_time)
|
|
532
546
|
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# Safety: If loading takes too long (> 8s), force reset to allow error handling/skip
|
|
550
|
+
# Consolidated redundancy checks into a single clean block
|
|
551
|
+
now = time.time()
|
|
552
|
+
if self.player.loading and (now - self.player.loading_ts > 8):
|
|
553
|
+
self.player.loading = False
|
|
554
|
+
self.status_msg = "⚠️ Load timed out. Skipping..."
|
|
555
|
+
|
|
533
556
|
# 2. Frequent: Pause state (Every 2 loops ~400ms)
|
|
534
557
|
if self.loop_count % 2 == 0:
|
|
535
558
|
p = self.player.get_property("pause")
|
|
@@ -549,10 +572,6 @@ class MyTunesApp:
|
|
|
549
572
|
is_idle = self.player.get_property("idle-active")
|
|
550
573
|
if is_idle and self.player.loading:
|
|
551
574
|
self.player.loading = False
|
|
552
|
-
|
|
553
|
-
# Timeout fallback for loading state (remains every loop logic)
|
|
554
|
-
if self.player.loading and (time.time() - getattr(self.player, 'loading_ts', 0) > 8):
|
|
555
|
-
self.player.loading = False
|
|
556
575
|
|
|
557
576
|
# Periodic Save (Throttle 10s)
|
|
558
577
|
if time.time() - getattr(self, 'last_save_time', 0) > 10:
|
|
@@ -577,6 +596,10 @@ class MyTunesApp:
|
|
|
577
596
|
|
|
578
597
|
if key == -1: return
|
|
579
598
|
|
|
599
|
+
# Reset Idle Timer
|
|
600
|
+
self.last_input_time = time.time()
|
|
601
|
+
|
|
602
|
+
|
|
580
603
|
# Handle formatting: invalid key might be int -1
|
|
581
604
|
|
|
582
605
|
# Resize Info
|
|
@@ -587,10 +610,37 @@ class MyTunesApp:
|
|
|
587
610
|
|
|
588
611
|
# GLOBAL ESC: Background Play (Exit but keep music)
|
|
589
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
|
|
590
614
|
if key == 27 or key == '\x1b':
|
|
591
|
-
|
|
592
|
-
self.
|
|
593
|
-
|
|
615
|
+
# Peek for next key with very short timeout
|
|
616
|
+
self.stdscr.timeout(50)
|
|
617
|
+
try:
|
|
618
|
+
next_key = self.stdscr.getch()
|
|
619
|
+
if next_key == 127: # Backspace
|
|
620
|
+
# This is Option+Backspace -> Treat as DELETE
|
|
621
|
+
key = curses.KEY_DC # Transform to Delete Key
|
|
622
|
+
else:
|
|
623
|
+
# If valid key but not 127, put it back or handle?
|
|
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)
|
|
594
644
|
|
|
595
645
|
# Handle Mouse Click
|
|
596
646
|
if key == curses.KEY_MOUSE:
|
|
@@ -626,9 +676,10 @@ class MyTunesApp:
|
|
|
626
676
|
current_list = self.get_current_list()
|
|
627
677
|
|
|
628
678
|
# Navigation logic
|
|
629
|
-
# Back: Q, Left Arrow, Backspace,
|
|
679
|
+
# Back: Q, Left Arrow, Backspace, h, 6
|
|
680
|
+
# Fix: Removed Korean mappings ('ㅂ', 'ㅗ') to prevent IME ghost keys per user request
|
|
630
681
|
if key == curses.KEY_LEFT or key == curses.KEY_BACKSPACE or key == 127 or \
|
|
631
|
-
k_char in ['q', '
|
|
682
|
+
k_char in ['q', '6', 'h']:
|
|
632
683
|
if len(self.view_stack) > 1:
|
|
633
684
|
# Pop current view and push to forward stack
|
|
634
685
|
current_view = self.view_stack.pop()
|
|
@@ -641,7 +692,8 @@ class MyTunesApp:
|
|
|
641
692
|
|
|
642
693
|
# Forward: L, Right Arrow (Browser Style)
|
|
643
694
|
# Re-visit the view we just popped from
|
|
644
|
-
|
|
695
|
+
# Fix: Removed Korean mappings ('ㅣ') to prevent IME ghost keys
|
|
696
|
+
if k_char in ['l', 'L'] or key == curses.KEY_RIGHT:
|
|
645
697
|
if self.forward_stack:
|
|
646
698
|
next_view = self.forward_stack.pop()
|
|
647
699
|
self.view_stack.append(next_view)
|
|
@@ -649,7 +701,10 @@ class MyTunesApp:
|
|
|
649
701
|
self.status_msg = ""
|
|
650
702
|
return
|
|
651
703
|
|
|
652
|
-
|
|
704
|
+
return
|
|
705
|
+
|
|
706
|
+
# Fix: Removed Korean mappings ('ㅏ', 'ㅓ') for stability
|
|
707
|
+
if key == curses.KEY_UP or k_char in ['k']:
|
|
653
708
|
if self.selection_idx > 0:
|
|
654
709
|
self.selection_idx -= 1
|
|
655
710
|
if self.selection_idx < self.scroll_offset: self.scroll_offset = self.selection_idx
|
|
@@ -660,7 +715,7 @@ class MyTunesApp:
|
|
|
660
715
|
# Maintain scroll consistency (h - 10 matches draw() layout)
|
|
661
716
|
list_area_height = h - 10
|
|
662
717
|
self.scroll_offset = max(0, self.selection_idx - list_area_height + 1)
|
|
663
|
-
elif key == curses.KEY_DOWN or k_char in ['j'
|
|
718
|
+
elif key == curses.KEY_DOWN or k_char in ['j']:
|
|
664
719
|
if self.selection_idx < len(current_list) - 1:
|
|
665
720
|
self.selection_idx += 1
|
|
666
721
|
h, _ = self.stdscr.getmaxyx()
|
|
@@ -672,26 +727,29 @@ class MyTunesApp:
|
|
|
672
727
|
self.selection_idx = 0
|
|
673
728
|
self.scroll_offset = 0
|
|
674
729
|
|
|
675
|
-
# Enter / Select
|
|
676
|
-
elif key
|
|
677
|
-
|
|
730
|
+
# Enter / Select Logic
|
|
731
|
+
elif key in ['\n', '\r', 10, 13, curses.KEY_ENTER]:
|
|
732
|
+
# v2.0.3 Stability: Debounce Enter to prevent double-firing
|
|
733
|
+
if time.time() - getattr(self, 'last_enter_time', 0) > 0.3:
|
|
734
|
+
self.last_enter_time = time.time()
|
|
735
|
+
self.activate_selection(current_list)
|
|
678
736
|
|
|
679
|
-
# Shortcuts
|
|
680
|
-
# Search: S,
|
|
681
|
-
elif k_char in ['s', 'S', '
|
|
737
|
+
# Shortcuts: Number keys & English letters (Strict Mode)
|
|
738
|
+
# Search: S, 1, /
|
|
739
|
+
elif k_char in ['s', 'S', '1', '/'] and (not isinstance(key, str) or key.isprintable()):
|
|
682
740
|
self.forward_stack = [] # Clear forward history on new navigation
|
|
683
741
|
self.prompt_search()
|
|
684
742
|
|
|
685
|
-
# Favorites: F,
|
|
686
|
-
elif k_char in ['f', 'F', '
|
|
743
|
+
# Favorites: F, 2
|
|
744
|
+
elif k_char in ['f', 'F', '2']:
|
|
687
745
|
if self.view_stack[-1] != "favorites":
|
|
688
746
|
self.forward_stack = []
|
|
689
747
|
self.view_stack.append("favorites")
|
|
690
748
|
self.selection_idx = 0
|
|
691
749
|
self.status_msg = self.t("favorites_info", DATA_FILE)
|
|
692
750
|
|
|
693
|
-
# History: R,
|
|
694
|
-
elif k_char in ['r', 'R', '
|
|
751
|
+
# History: R, 3 (Changed from H to avoid Back conflict)
|
|
752
|
+
elif k_char in ['r', 'R', '3']:
|
|
695
753
|
if self.view_stack[-1] != "history":
|
|
696
754
|
self.forward_stack = []
|
|
697
755
|
self.cached_history = list(self.dm.data['history']) # Snapshot
|
|
@@ -699,8 +757,8 @@ class MyTunesApp:
|
|
|
699
757
|
self.selection_idx = 0
|
|
700
758
|
self.status_msg = self.t("hist_info")
|
|
701
759
|
|
|
702
|
-
# Main Menu: M,
|
|
703
|
-
elif k_char in ['m', 'M', '
|
|
760
|
+
# Main Menu: M, 4
|
|
761
|
+
elif k_char in ['m', 'M', '4']:
|
|
704
762
|
self.forward_stack = [] # Clear forward history
|
|
705
763
|
self.view_stack = ["main"]; self.selection_idx = 0; self.scroll_offset = 0; self.status_msg = ""
|
|
706
764
|
|
|
@@ -750,7 +808,7 @@ class MyTunesApp:
|
|
|
750
808
|
self.status_msg = "⚠️ Already Shared Recently!"
|
|
751
809
|
else:
|
|
752
810
|
try:
|
|
753
|
-
# Send to
|
|
811
|
+
# Send to Serverless Proxy (Secure)
|
|
754
812
|
payload = {
|
|
755
813
|
"title": title,
|
|
756
814
|
"url": url,
|
|
@@ -758,19 +816,38 @@ class MyTunesApp:
|
|
|
758
816
|
"country": self.dm.get_country(),
|
|
759
817
|
"timestamp": time.time()
|
|
760
818
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
819
|
+
|
|
820
|
+
# v1.9.9 Security Update: Use centralized API with Auth Header
|
|
821
|
+
# v2.0.0 Threading for Smoothness
|
|
822
|
+
def send_share_async(payload, headers, url_to_share, title_to_share):
|
|
823
|
+
try:
|
|
824
|
+
resp = requests.post(
|
|
825
|
+
self.share_api_url,
|
|
826
|
+
json=payload,
|
|
827
|
+
headers=headers,
|
|
828
|
+
timeout=3
|
|
829
|
+
)
|
|
830
|
+
if resp.status_code == 200:
|
|
831
|
+
self.sent_history[url_to_share] = time.time()
|
|
832
|
+
safe_t = self.truncate(title_to_share, 50)
|
|
833
|
+
self.status_msg = f"🚀 Shared: {safe_t}..."
|
|
834
|
+
else:
|
|
835
|
+
self.status_msg = f"❌ Share Error: {resp.status_code}"
|
|
836
|
+
except:
|
|
837
|
+
self.status_msg = "❌ Network Error (API)"
|
|
838
|
+
|
|
839
|
+
headers = {
|
|
840
|
+
"Content-Type": "application/json",
|
|
841
|
+
"x-mytunes-secret": "mytunes-v1-secret-8822"
|
|
842
|
+
}
|
|
843
|
+
threading.Thread(target=send_share_async, args=(payload, headers, url, title), daemon=True).start()
|
|
844
|
+
|
|
768
845
|
except Exception as e:
|
|
769
846
|
self.status_msg = f"❌ Share Failed: {str(e)}"
|
|
770
847
|
|
|
771
848
|
|
|
772
|
-
# Add to Favorites: A,
|
|
773
|
-
elif k_char in ['a', 'A', '
|
|
849
|
+
# Add to Favorites: A, 5
|
|
850
|
+
elif k_char in ['a', 'A', '5']:
|
|
774
851
|
if current_list and 0 <= self.selection_idx < len(current_list):
|
|
775
852
|
target_item = current_list[self.selection_idx]
|
|
776
853
|
# Ensure it's a valid track item (has url)
|
|
@@ -791,84 +868,55 @@ class MyTunesApp:
|
|
|
791
868
|
self.status_msg = "🌐 Opening YouTube in Browser..."
|
|
792
869
|
threading.Thread(target=webbrowser.open, args=(url,), daemon=True).start()
|
|
793
870
|
|
|
794
|
-
# Open Live Station (F8): App Mode with Optimized Flags (v1.8.6)
|
|
795
871
|
elif key == curses.KEY_F8:
|
|
796
|
-
|
|
872
|
+
homepage_url = "https://mytunes-pro.com"
|
|
797
873
|
if self.is_remote():
|
|
798
|
-
self.show_copy_dialog("
|
|
874
|
+
self.show_copy_dialog("MyTunes Home", homepage_url)
|
|
799
875
|
return
|
|
800
876
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
# We switch to the standard `webbrowser` module which handles system default browser reliably.
|
|
804
|
-
# This sacrifices window sizing but guarantees the URL opens.
|
|
805
|
-
if self.is_wsl():
|
|
806
|
-
threading.Thread(target=webbrowser.open, args=(live_url,), daemon=True).start()
|
|
807
|
-
return
|
|
877
|
+
self.status_msg = "🌐 Opening MyTunes Home..."
|
|
878
|
+
threading.Thread(target=webbrowser.open, args=(homepage_url,), daemon=True).start()
|
|
808
879
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
os.path.join(os.environ.get('PROGRAMFILES(X86)', ''), 'Google\\Chrome\\Application\\chrome.exe'),
|
|
848
|
-
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Google\\Chrome\\Application\\chrome.exe'),
|
|
849
|
-
]
|
|
850
|
-
for p in win_paths:
|
|
851
|
-
if p and os.path.exists(p):
|
|
852
|
-
try:
|
|
853
|
-
subprocess.Popen([p] + flags, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
854
|
-
launched = True; break
|
|
855
|
-
except: pass
|
|
856
|
-
|
|
880
|
+
# Delete Item: DEL, d
|
|
881
|
+
elif key == curses.KEY_DC or k_char in ['d']:
|
|
882
|
+
if current_list and 0 <= self.selection_idx < len(current_list):
|
|
883
|
+
view = self.view_stack[-1]
|
|
884
|
+
success = False
|
|
885
|
+
|
|
886
|
+
if view == "favorites":
|
|
887
|
+
success = self.dm.remove_favorite_by_index(self.selection_idx)
|
|
888
|
+
if success: self.status_msg = "🗑️ Deleted from Favorites"
|
|
889
|
+
|
|
890
|
+
elif view == "history":
|
|
891
|
+
success = self.dm.remove_history_by_index(self.selection_idx)
|
|
892
|
+
if success:
|
|
893
|
+
self.cached_history = list(self.dm.data['history']) # Refresh view
|
|
894
|
+
self.status_msg = "🗑️ Deleted from History"
|
|
895
|
+
|
|
896
|
+
elif view == "search":
|
|
897
|
+
# If current_search_query is None, we are viewing Search History
|
|
898
|
+
if self.current_search_query is None:
|
|
899
|
+
success = self.dm.remove_search_history_by_index(self.selection_idx)
|
|
900
|
+
if success:
|
|
901
|
+
self.search_results = self.dm.get_search_history() # Refresh
|
|
902
|
+
self.status_msg = "🗑️ Deleted from Search History"
|
|
903
|
+
else:
|
|
904
|
+
# Ephemeral removal from result list
|
|
905
|
+
try:
|
|
906
|
+
self.search_results.pop(self.selection_idx)
|
|
907
|
+
self.status_msg = "Start new search"
|
|
908
|
+
success = True
|
|
909
|
+
except: pass
|
|
910
|
+
|
|
911
|
+
if success:
|
|
912
|
+
# Adjust selection index if out of bounds
|
|
913
|
+
# If list became empty, idx will be 0 but len is 0.
|
|
914
|
+
# We just need to ensure we don't crash next draw.
|
|
915
|
+
# The draw logic (get_current_list) handles empty lists safely.
|
|
916
|
+
if self.selection_idx >= len(self.get_current_list()):
|
|
917
|
+
self.selection_idx = max(0, len(self.get_current_list()) - 1)
|
|
857
918
|
|
|
858
|
-
# 4. Native Linux
|
|
859
|
-
else:
|
|
860
|
-
for b in ['google-chrome', 'google-chrome-stable', 'brave-browser', 'chromium-browser', 'chromium']:
|
|
861
|
-
p = shutil.which(b)
|
|
862
|
-
if p:
|
|
863
|
-
try:
|
|
864
|
-
subprocess.Popen([p] + flags, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL); launched = True; break
|
|
865
|
-
except: pass
|
|
866
919
|
|
|
867
|
-
if launched:
|
|
868
|
-
self.status_msg = "📡 Opening Live Popup (712x800)..."
|
|
869
|
-
else:
|
|
870
|
-
webbrowser.open(live_url)
|
|
871
|
-
self.status_msg = "📡 Opening Live Station (Browser)..."
|
|
872
920
|
|
|
873
921
|
def ask_resume(self, saved_time, track_title):
|
|
874
922
|
self.stdscr.nodelay(False) # Blocking input for dialog
|
|
@@ -1009,13 +1057,9 @@ class MyTunesApp:
|
|
|
1009
1057
|
self.status_msg = "" # Clear stale messages on language switch
|
|
1010
1058
|
elif item["id"] == "quit": self.running = False
|
|
1011
1059
|
else:
|
|
1012
|
-
# Check for Load More Button
|
|
1013
|
-
if item.get("id") == "load_more_btn":
|
|
1014
|
-
self.load_more_results()
|
|
1015
|
-
return
|
|
1016
|
-
|
|
1017
1060
|
self.play_music(item, interactive=True)
|
|
1018
1061
|
|
|
1062
|
+
|
|
1019
1063
|
def play_music(self, item, interactive=True, preserve_queue=False):
|
|
1020
1064
|
if not item.get("url"): return # Guard against dummy items
|
|
1021
1065
|
|
|
@@ -1170,7 +1214,8 @@ class MyTunesApp:
|
|
|
1170
1214
|
if query:
|
|
1171
1215
|
self.status_msg = self.t("searching")
|
|
1172
1216
|
self.draw()
|
|
1173
|
-
|
|
1217
|
+
# v2.0.0 Refactor: Threaded Search
|
|
1218
|
+
threading.Thread(target=self.perform_search, args=(query,), daemon=True).start()
|
|
1174
1219
|
else:
|
|
1175
1220
|
# Revert if no query and we were just previewing history
|
|
1176
1221
|
# But requirement 2: "If Enter with no query, preserve previous search results"
|
|
@@ -1179,50 +1224,38 @@ class MyTunesApp:
|
|
|
1179
1224
|
# If the user wants to CANCEL and go back to Main, they might need ESC.
|
|
1180
1225
|
pass
|
|
1181
1226
|
|
|
1182
|
-
def perform_search(self, query
|
|
1227
|
+
def perform_search(self, query):
|
|
1183
1228
|
try:
|
|
1184
|
-
|
|
1185
|
-
if
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
# Resolve yt-dlp path: checks dirname of current python (venv/bin) first
|
|
1229
|
+
# v2.0.4 Fix: Don't set player.loading=True for Search.
|
|
1230
|
+
# It triggers playback timeout (skipping) logic if search is slow.
|
|
1231
|
+
# self.player.loading = True
|
|
1232
|
+
|
|
1233
|
+
self.current_search_query = query
|
|
1234
|
+
self.status_msg = self.t("searching")
|
|
1235
|
+
|
|
1236
|
+
# Resolve yt-dlp path
|
|
1194
1237
|
yt_dlp_cmd = "yt-dlp"
|
|
1195
1238
|
venv_bin = os.path.dirname(sys.executable)
|
|
1196
1239
|
venv_yt_dlp = os.path.join(venv_bin, "yt-dlp")
|
|
1197
1240
|
if os.path.exists(venv_yt_dlp) and os.access(venv_yt_dlp, os.X_OK):
|
|
1198
1241
|
yt_dlp_cmd = venv_yt_dlp
|
|
1199
1242
|
|
|
1200
|
-
#
|
|
1201
|
-
limit =
|
|
1202
|
-
# yt-dlp logic: ytsearchN asks for N results total.
|
|
1203
|
-
# to get page 2 (51-100), we ask for 100, checking playlist-items indices?
|
|
1204
|
-
# actually ytsearchN with --playlist-start START works.
|
|
1205
|
-
# We ask for (page * limit) because 'ytsearch' usually returns 'up to N'.
|
|
1206
|
-
# If we just ask for 50 with start 51, it might fail depending on yt-dlp version.
|
|
1207
|
-
# Safest is: ytsearch(page*limit) with --playlist-start ((page-1)*limit + 1)
|
|
1208
|
-
|
|
1209
|
-
total_fetch = page * limit
|
|
1210
|
-
start_index = (page - 1) * limit + 1
|
|
1211
|
-
|
|
1243
|
+
# v2.0.2 Optimization: 25 Items (Better space usage per user request)
|
|
1244
|
+
limit = 25
|
|
1212
1245
|
search_query = f"{query} music"
|
|
1213
1246
|
cmd = [
|
|
1214
1247
|
yt_dlp_cmd,
|
|
1215
|
-
f"ytsearch{
|
|
1216
|
-
"--dump-json", "--flat-playlist", "--no-playlist", "--skip-download"
|
|
1217
|
-
"--playlist-start", str(start_index)
|
|
1248
|
+
f"ytsearch{limit}:{search_query}",
|
|
1249
|
+
"--dump-json", "--flat-playlist", "--no-playlist", "--skip-download"
|
|
1218
1250
|
]
|
|
1219
1251
|
|
|
1220
1252
|
try:
|
|
1221
1253
|
result = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode('utf-8')
|
|
1222
1254
|
except subprocess.CalledProcessError:
|
|
1223
|
-
result = ""
|
|
1255
|
+
result = ""
|
|
1224
1256
|
|
|
1225
1257
|
new = []
|
|
1258
|
+
seen_urls = set()
|
|
1226
1259
|
for line in result.strip().split("\n"):
|
|
1227
1260
|
if line:
|
|
1228
1261
|
try:
|
|
@@ -1231,57 +1264,35 @@ class MyTunesApp:
|
|
|
1231
1264
|
if not url or "http" not in url: url = f"https://www.youtube.com/watch?v={d.get('id')}"
|
|
1232
1265
|
dur = d.get("duration", 0)
|
|
1233
1266
|
dur_str = f"{int(dur)//60}:{int(dur)%60:02d}" if dur else ""
|
|
1234
|
-
|
|
1267
|
+
# Dedup Check
|
|
1268
|
+
if url not in seen_urls:
|
|
1269
|
+
seen_urls.add(url)
|
|
1270
|
+
new.append({"title": d.get("title", "Unknown"), "url": url, "duration": dur_str})
|
|
1235
1271
|
except: pass
|
|
1236
1272
|
|
|
1273
|
+
# Enforce hard limit
|
|
1274
|
+
new = new[:limit]
|
|
1275
|
+
|
|
1237
1276
|
if new:
|
|
1238
|
-
|
|
1239
|
-
if self.
|
|
1240
|
-
self.
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
#
|
|
1244
|
-
|
|
1245
|
-
load_more_item = {
|
|
1246
|
-
"title": "[ Next 50 Results... ]" if self.lang == 'en' else "[ 다음 50개 더 보기... ]",
|
|
1247
|
-
"id": "load_more_btn",
|
|
1248
|
-
"url": "", # Dummy
|
|
1249
|
-
"duration": ""
|
|
1250
|
-
}
|
|
1251
|
-
new.append(load_more_item)
|
|
1252
|
-
|
|
1253
|
-
if page == 1:
|
|
1254
|
-
self.search_results = new
|
|
1255
|
-
if self.view_stack[-1] != "search":
|
|
1256
|
-
self.view_stack.append("search")
|
|
1257
|
-
self.selection_idx = 0; self.scroll_offset = 0
|
|
1258
|
-
|
|
1259
|
-
# SAVE to History (Exclude load_more_btn)
|
|
1260
|
-
items_to_save = [x for x in new if x.get('id') != 'load_more_btn']
|
|
1261
|
-
self.dm.add_search_results(items_to_save)
|
|
1262
|
-
|
|
1263
|
-
else:
|
|
1264
|
-
self.search_results.extend(new)
|
|
1265
|
-
# Also save subsequent pages to history
|
|
1266
|
-
items_to_save = [x for x in new if x.get('id') != 'load_more_btn']
|
|
1267
|
-
self.dm.add_search_results(items_to_save)
|
|
1277
|
+
self.search_results = new
|
|
1278
|
+
if self.view_stack[-1] != "search":
|
|
1279
|
+
self.view_stack.append("search")
|
|
1280
|
+
self.selection_idx = 0; self.scroll_offset = 0
|
|
1281
|
+
|
|
1282
|
+
# SAVE to History
|
|
1283
|
+
self.dm.add_search_results(new)
|
|
1268
1284
|
|
|
1269
|
-
self.
|
|
1270
|
-
self.status_msg = f"Search Done. ({len(self.search_results)-1})" # -1 for button
|
|
1285
|
+
self.status_msg = f"Search Done. ({len(new)} results)"
|
|
1271
1286
|
else:
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
self.status_msg = "No more results."
|
|
1275
|
-
# Remove button if no more
|
|
1276
|
-
if self.search_results and self.search_results[-1].get("id") == "load_more_btn":
|
|
1277
|
-
self.search_results.pop()
|
|
1287
|
+
self.status_msg = self.t("no_results")
|
|
1288
|
+
|
|
1278
1289
|
except Exception as e: self.status_msg = f"Error: {e}"
|
|
1279
1290
|
finally:
|
|
1280
|
-
self.
|
|
1291
|
+
self.player.loading = False
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
|
|
1281
1295
|
|
|
1282
|
-
def load_more_results(self):
|
|
1283
|
-
if self.current_search_query and not self.is_loading_more:
|
|
1284
|
-
self.perform_search(self.current_search_query, self.search_page + 1)
|
|
1285
1296
|
|
|
1286
1297
|
def draw(self):
|
|
1287
1298
|
self.stdscr.erase()
|
|
@@ -1437,18 +1448,15 @@ class MyTunesApp:
|
|
|
1437
1448
|
|
|
1438
1449
|
def check_autoplay(self):
|
|
1439
1450
|
# Auto-play next track from Global Queue
|
|
1451
|
+
# Guard: Don't autoplay if we are currently loading a track
|
|
1452
|
+
if self.player.loading: return
|
|
1453
|
+
|
|
1440
1454
|
try:
|
|
1441
1455
|
is_idle = self.player.get_property("idle-active")
|
|
1442
1456
|
if is_idle and self.current_track and self.queue:
|
|
1443
1457
|
if self.queue_idx + 1 < len(self.queue):
|
|
1444
1458
|
self.queue_idx += 1
|
|
1445
1459
|
next_item = self.queue[self.queue_idx]
|
|
1446
|
-
|
|
1447
|
-
if next_item.get('id') == 'load_more_btn':
|
|
1448
|
-
# TODO: Auto-trigger load more? For now just stop.
|
|
1449
|
-
self.current_track = None
|
|
1450
|
-
return
|
|
1451
|
-
|
|
1452
1460
|
try: self.play_music(next_item, interactive=False, preserve_queue=True)
|
|
1453
1461
|
except: pass
|
|
1454
1462
|
else:
|
|
@@ -1463,6 +1471,14 @@ class MyTunesApp:
|
|
|
1463
1471
|
self.check_autoplay()
|
|
1464
1472
|
self.draw()
|
|
1465
1473
|
self.handle_input()
|
|
1474
|
+
|
|
1475
|
+
# Idle / Sleep Check
|
|
1476
|
+
# If no input for 60s and Paused, slow down loop
|
|
1477
|
+
if time.time() - getattr(self, 'last_input_time', 0) > 60 and self.is_paused:
|
|
1478
|
+
self.stdscr.timeout(1000)
|
|
1479
|
+
else:
|
|
1480
|
+
self.stdscr.timeout(200)
|
|
1481
|
+
|
|
1466
1482
|
except Exception as e:
|
|
1467
1483
|
# v1.8.4 - Global resilience: Catch and log loop errors instead of crashing
|
|
1468
1484
|
try:
|
|
@@ -1472,6 +1488,10 @@ class MyTunesApp:
|
|
|
1472
1488
|
# Small sleep to prevent infinite tight loop on persistent error
|
|
1473
1489
|
time.sleep(0.1)
|
|
1474
1490
|
|
|
1491
|
+
# Cleanup Mouse (Prevent terminal artifacts)
|
|
1492
|
+
try: curses.mousemask(0)
|
|
1493
|
+
except: pass
|
|
1494
|
+
|
|
1475
1495
|
if self.stop_on_exit:
|
|
1476
1496
|
self.player.stop()
|
|
1477
1497
|
self.player.cleanup_orphaned_mpv()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mytunes-pro
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.1
|
|
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
|
|
@@ -19,9 +19,11 @@ Dynamic: license-file
|
|
|
19
19
|
|
|
20
20
|
# 🎵 MyTunes Pro (Korean)
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
## 🚀 Professional TUI Music Player v2.0.1
|
|
23
|
+
|
|
24
|
+
MyTunes Pro는 **Context7**의 심층 리서치를 기반으로 제작된 **Premium CLI Music Player**입니다.
|
|
25
|
+
Python `curses` 라이브러리를 사용하여 터미널 환경에서도 **GUI급의 유려한 UX**를 제공하며,
|
|
26
|
+
`mpv` 플레이어의 강력한 IPC 제어 기능을 통해 끊김 없는 백그라운드 재생을 지원합니다.
|
|
25
27
|
|
|
26
28
|
> **💡 개발 배경**
|
|
27
29
|
> 이 프로그램은 하루 종일 터미널을 보는 개발자들이 **작업 흐름을 끊지 않고** 편하게 음악을 듣기 위해 만들어졌습니다.
|
|
@@ -182,19 +184,21 @@ Windows 환경에서 한글 검색이 안 되거나 설치가 어려운 분들
|
|
|
182
184
|
| **`F7`** | **유튜브 열기** | 현재 곡을 브라우저에서 보기 |
|
|
183
185
|
| **`F8`** | **라이브 (Live)** | **실시간 음악 대시보드 열기** (전용 팝업창) |
|
|
184
186
|
| **`F9`** | **공유 (Share)** | **현재 곡을 라이브 스테이션에 즉시 공유** |
|
|
185
|
-
| **`6`** | **뒤로가기** | 이전 화면으로 이동 (단축키 `Q`, `
|
|
187
|
+
| **`6`** | **뒤로가기** | 이전 화면으로 이동 (단축키 `Q`, `h`와 동일) |
|
|
188
|
+
| **`L`** | **앞으로** | 이전 화면에서 앞화면으로 다시 이동 (`Right Arrow`) |
|
|
186
189
|
| **`ESC`** | **배경재생** | **음악 끄지 않고 나가기** (백그라운드 재생) |
|
|
187
190
|
|
|
188
191
|
### 🧭 기본 탐색
|
|
189
192
|
| 키 | 동작 |
|
|
190
193
|
| :--- | :--- |
|
|
191
194
|
| `↑` / `↓` / `k` / `j` | 리스트 위/아래 이동 (Vim 키 지원) |
|
|
192
|
-
| `Enter` / `l` | **선택 / 재생**
|
|
195
|
+
| `Enter` / `l` | **선택 / 재생** |
|
|
193
196
|
| `Space` | 재생 / 일시정지 (Play/Pause) |
|
|
194
197
|
| `-` / `+` | **볼륨 조절** (- / +) |
|
|
195
198
|
| `,` / `.` | 10초 뒤로 / 앞으로 감기 |
|
|
196
199
|
| `<` / `>` | **30초** 뒤로 / 앞으로 감기 (Shift) |
|
|
197
200
|
| `Backspace` / `h` / `q` | 뒤로 가기 / 검색어 지우기 |
|
|
201
|
+
| `L` | **앞으로 가기** |
|
|
198
202
|
| `/` | **검색** (Vim Style) |
|
|
199
203
|
|
|
200
204
|
---
|
|
@@ -208,7 +212,7 @@ Windows 환경에서 한글 검색이 안 되거나 설치가 어려운 분들
|
|
|
208
212
|
|
|
209
213
|
# 🎵 MyTunes Pro (English)
|
|
210
214
|
|
|
211
|
-
**Modern CLI YouTube Music Player (
|
|
215
|
+
**Modern CLI YouTube Music Player (v2.0.1)**
|
|
212
216
|
A lightweight, keyboard-centric terminal player for streaming YouTube music.
|
|
213
217
|
|
|
214
218
|
---
|
|
@@ -289,13 +293,21 @@ sudo apt install mpv python3 python3-pip pipx python3-venv -y
|
|
|
289
293
|
| **`5`** | **Add/Del** | Toggle Favorite (Same as `A`) |
|
|
290
294
|
| **`+`** | **Vol Up** | Volume +5% (Same as `=`) |
|
|
291
295
|
| **`-`** | **Vol Down** | Volume -5% (Same as `_`) |
|
|
292
|
-
| **`6`** | **Back** | Go back (Same as `Q`, `
|
|
296
|
+
| **`6`** | **Back** | Go back (Same as `Q`, `h`) |
|
|
297
|
+
| **`L`** | **Forward** | Go forward (`Right Arrow`) |
|
|
293
298
|
| **`ESC`** | **Bg Play** | **Exit app but keep music playing** |
|
|
294
299
|
|
|
295
300
|
---
|
|
296
301
|
|
|
297
302
|
## 🔄 Changelog
|
|
298
303
|
|
|
304
|
+
### v2.0.1 (Keymap Refinement & Version Sync)
|
|
305
|
+
|
|
306
|
+
- **Navigation**: Added browser-style Forward navigation (`L` / `Right Arrow`).
|
|
307
|
+
- **Keybinding Optimization**: Updated History mapping to `R` / `3` and refined Back/Forward logic.
|
|
308
|
+
- **IME Stability**: Removed unstable Korean character mappings (`ㄴ`, `ㄹ`, `ㄱ`, 등) to prevent ghost key issues in the TUI.
|
|
309
|
+
- **Global Synchronization**: Synchronized version v2.0.1 across CLI, TUI, and Web interfaces.
|
|
310
|
+
|
|
299
311
|
### v1.9.9 (Domain Migration & Realtime Sync)
|
|
300
312
|
|
|
301
313
|
- **Domain Migration**: Updated all branding and internal links to support `mytunes-pro.com`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mytunes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
mytunes/app.py,sha256=i6G_1_8guGJqc_EvzlVzMpYfPSSZL1jB7_Nb30qX1T4,61619
|
|
3
|
+
mytunes_pro-2.0.1.dist-info/licenses/LICENSE,sha256=lOrP0EIjxcgJia__W3f3PVDZkRd2oRzFkyH2g3LRRCg,1063
|
|
4
|
+
mytunes_pro-2.0.1.dist-info/METADATA,sha256=fqpGMPvQnZxzvgaViPMVrvNPEy29yHV0mH3zT59hyKY,22317
|
|
5
|
+
mytunes_pro-2.0.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
6
|
+
mytunes_pro-2.0.1.dist-info/entry_points.txt,sha256=6-MsC13nIgzLvrREaGotc32FgxHx_Iuu1z2qCzJs1_4,65
|
|
7
|
+
mytunes_pro-2.0.1.dist-info/top_level.txt,sha256=KWzdFyNNG_sO7GT83-sN5fYArP4_DL5I8HYIwgazXyY,8
|
|
8
|
+
mytunes_pro-2.0.1.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
mytunes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
mytunes/app.py,sha256=aTfLWNu5i35h2UBXobIEGR03bBd9t6XowYdc_ggZk18,61028
|
|
3
|
-
mytunes_pro-1.9.9.dist-info/licenses/LICENSE,sha256=lOrP0EIjxcgJia__W3f3PVDZkRd2oRzFkyH2g3LRRCg,1063
|
|
4
|
-
mytunes_pro-1.9.9.dist-info/METADATA,sha256=-E4osRv8m7ZDhmhUVLHFiRj8Sa-v5kllG8oAZsq-vfs,21615
|
|
5
|
-
mytunes_pro-1.9.9.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
6
|
-
mytunes_pro-1.9.9.dist-info/entry_points.txt,sha256=6-MsC13nIgzLvrREaGotc32FgxHx_Iuu1z2qCzJs1_4,65
|
|
7
|
-
mytunes_pro-1.9.9.dist-info/top_level.txt,sha256=KWzdFyNNG_sO7GT83-sN5fYArP4_DL5I8HYIwgazXyY,8
|
|
8
|
-
mytunes_pro-1.9.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|