asd
This commit is contained in:
		
							parent
							
								
									e05b00609c
								
							
						
					
					
						commit
						0ea98848e5
					
				
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
/out
 | 
			
		||||
/music_mgr/target
 | 
			
		||||
/.venv
 | 
			
		||||
/out
 | 
			
		||||
/music_mgr/target
 | 
			
		||||
/.venv
 | 
			
		||||
/config.json
 | 
			
		||||
| 
						 | 
				
			
			@ -1,13 +1,8 @@
 | 
			
		|||
{
 | 
			
		||||
  "ytdlp": {
 | 
			
		||||
    "path": "/usr/bin/yt-dlp",
 | 
			
		||||
    "is_python": false
 | 
			
		||||
    "path": "C:\\bin\\yt-dlp.exe"
 | 
			
		||||
  },
 | 
			
		||||
  "spotdl": {
 | 
			
		||||
    "path": "/home/mcorange/.local/bin/spotdl",
 | 
			
		||||
    "is_python": false
 | 
			
		||||
  },
 | 
			
		||||
  "python": {
 | 
			
		||||
    "path": ""
 | 
			
		||||
    "path": "UNUSED"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1264
									
								
								manifest.json
									
									
									
									
									
								
							
							
						
						
									
										1264
									
								
								manifest.json
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										3628
									
								
								music_mgr/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3628
									
								
								music_mgr/Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -1,22 +1,22 @@
 | 
			
		|||
[package]
 | 
			
		||||
name = "music_mgr"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
anstyle = "1.0.6"
 | 
			
		||||
anyhow = "1.0.81"
 | 
			
		||||
camino = "1.1.6"
 | 
			
		||||
clap = { version = "4.5.4", features = ["derive"] }
 | 
			
		||||
env_logger = "0.11.3"
 | 
			
		||||
lazy_static = "1.4.0"
 | 
			
		||||
libc = "0.2.153"
 | 
			
		||||
log = "0.4.21"
 | 
			
		||||
reqwest = "0.12.3"
 | 
			
		||||
serde = { version = "1.0.197", features = ["derive"] }
 | 
			
		||||
serde_json = "1.0.115"
 | 
			
		||||
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread", "process", "sync"] }
 | 
			
		||||
windows = { version = "0.56.0", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_Console"] }
 | 
			
		||||
zip-extensions = "0.6.2"
 | 
			
		||||
[package]
 | 
			
		||||
name = "music_mgr"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2021"
 | 
			
		||||
 | 
			
		||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
anstyle = "1.0.6"
 | 
			
		||||
anyhow = "1.0.81"
 | 
			
		||||
camino = "1.1.6"
 | 
			
		||||
clap = { version = "4.5.4", features = ["derive"] }
 | 
			
		||||
env_logger = "0.11.3"
 | 
			
		||||
lazy_static = "1.4.0"
 | 
			
		||||
libc = "0.2.153"
 | 
			
		||||
log = "0.4.21"
 | 
			
		||||
reqwest = "0.12.3"
 | 
			
		||||
serde = { version = "1.0.197", features = ["derive"] }
 | 
			
		||||
serde_json = "1.0.115"
 | 
			
		||||
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread", "process", "sync"] }
 | 
			
		||||
windows = { version = "0.56.0", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_Console"] }
 | 
			
		||||
zip-extensions = "0.6.2"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										145
									
								
								music_mgr/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								music_mgr/manifest.json
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,145 @@
 | 
			
		|||
{
 | 
			
		||||
    "format": "m4a",
 | 
			
		||||
    "genres": {
 | 
			
		||||
        "pop": [
 | 
			
		||||
            {"name": "Green Day - Basket Case", "url": "https://www.youtube.com/watch?v=wZ8eZRxFA-0"},
 | 
			
		||||
            {"name": "Icona Pop - I Love It", "url": "https://www.youtube.com/watch?v=UxxajLWwzqY"}
 | 
			
		||||
        ],
 | 
			
		||||
        "hip-hop": [
 | 
			
		||||
            {"name": "Afroman - Because I Got High", "url": "https://www.youtube.com/watch?v=WeYsTmIzjkw"}
 | 
			
		||||
        ],
 | 
			
		||||
        "rave": [
 | 
			
		||||
            {"name": "EVERYTHING WHAT", "url": "https://www.youtube.com/watch?v=Gjdsq4kc5cA"},
 | 
			
		||||
            {"name": "Tricky Disco", "url": "https://www.youtube.com/watch?v=t78qVdbAiXw"},
 | 
			
		||||
            {"name": "DR. VODKA - Tricky Disco", "url": "https://www.youtube.com/watch?v=IknAUhl3i2o"}
 | 
			
		||||
        ],
 | 
			
		||||
        "techno": [
 | 
			
		||||
            {"name": "Dance For Me", "url": "https://www.youtube.com/watch?v=5DTSvGO_944"},
 | 
			
		||||
            {"name": "Give It To Me", "url": "https://www.youtube.com/watch?v=upQe8EeSyZU"},
 | 
			
		||||
            {"name": "Empire Of The Sun, southstar - We Are The People", "url": "https://www.youtube.com/watch?v=qguEGR5BK2k"},
 | 
			
		||||
            {"name": "Beggin' (Techno)", "url": "https://www.youtube.com/watch?v=tXPs1FwW6lk"},
 | 
			
		||||
            {"name": "Lily Allen - Not Fair", "url": "https://www.youtube.com/watch?v=WON_YIbeLis"},
 | 
			
		||||
            {"name": "I WAS MADE FOR LOVIN' YOU (TECHNO)", "url": "https://www.youtube.com/watch?v=asVznhccYao"},
 | 
			
		||||
            {"name": "Nicolas Julian - Applause", "url": "https://www.youtube.com/watch?v=-pXlrWVICAE"},
 | 
			
		||||
            {"name": "08 Blumchen - Blaue Augen", "url": "https://www.youtube.com/watch?v=mE4PZcUfiwE"},
 | 
			
		||||
            {"name": "MUTA - Party maker", "url": "https://www.youtube.com/watch?v=LT9VNK1aCXY"}
 | 
			
		||||
        ],
 | 
			
		||||
        "electronic": [
 | 
			
		||||
            {"name": "Zombie Nation - Kernkraft 400", "url": "https://www.youtube.com/watch?v=z5LW07FTJbI"},
 | 
			
		||||
            {"name": "Benny Benassi - Satisfaction", "url": "https://www.youtube.com/watch?v=a0fkNdPiIL4"}
 | 
			
		||||
            
 | 
			
		||||
        ],
 | 
			
		||||
        "rock": [
 | 
			
		||||
            {"name": "Black Sabbath", "url": "https://www.youtube.com/watch?v=BOTIIw76qiE"}
 | 
			
		||||
        ],
 | 
			
		||||
        "house": [
 | 
			
		||||
            {"name": "Ralph Castelli - Morning Sex (Mochakk Remix)", "url": "https://www.youtube.com/watch?v=6bCwJ_TIDG4"},
 | 
			
		||||
            {"name": "Billie Eilish - Bossa Nova (Lewii Edit)", "url": "https://www.youtube.com/watch?v=gNawHj2NCxA"},
 | 
			
		||||
            {"name": "Fidde - I Only See Things I Dont Have", "url": "https://www.youtube.com/watch?v=vX_Ye_ZzI-Y"},
 | 
			
		||||
            {"name": "Bauhouse - After Marvins Dance (Marvin Gaye's 'After The Dance' Edit)", "url": "https://www.youtube.com/watch?v=J-cgyYiExh8"},
 | 
			
		||||
            {"name": "Men I Trust - Tailwhip (Lewii Edit)", "url": "https://www.youtube.com/watch?v=XhyM-JUWwWQ"},
 | 
			
		||||
            {"name": "Sweely - Le Son Dancefloor", "url": "https://www.youtube.com/watch?v=5uEvZgmoG6Y"},
 | 
			
		||||
            {"name": "THEOS - Rhodes Trip", "url": "https://www.youtube.com/watch?v=m7guRO0Uz_c"},
 | 
			
		||||
            {"name": "Baltra - Tears Drop", "url": "https://www.youtube.com/watch?v=EXXMtKPfuzY"},
 | 
			
		||||
            {"name": "Fidde - If Theres A Heaven I Wanna See It", "url": "https://www.youtube.com/watch?v=l2Nw7cIh7qg"},
 | 
			
		||||
            {"name": "Unknown Artist - Kcik 23", "url": "https://www.youtube.com/watch?v=SnnqDdZJpzA"}
 | 
			
		||||
        ],
 | 
			
		||||
        "lietuviskos": [
 | 
			
		||||
            {"name": "Adomas Vysniauskas - As Judu", "url": "https://www.youtube.com/watch?v=dMm16TzZrjg"},
 | 
			
		||||
            {"name": "RADVIS - KINO FILMAI", "url": "https://www.youtube.com/watch?v=vhAEkC3xNMo"},
 | 
			
		||||
            {"name": "16Hz - Autostrada Vilnius - Kaunas", "url": "https://www.youtube.com/watch?v=ANS2TSegr40"},
 | 
			
		||||
            {"name": "Zas - Zalias Pasas", "url": "https://www.youtube.com/watch?v=SZA7IjlCfyI"},
 | 
			
		||||
            {"name": "Dzordana Butkute - Nebenoriu Laukt", "url": "https://www.youtube.com/watch?v=_AozFrAqNMk"},
 | 
			
		||||
            {"name": "Juodas Garvezys (Remix)", "url": "https://www.youtube.com/watch?v=D-7qQbXHSAw"},
 | 
			
		||||
            {"name": "morre - Kaip Diena", "url": "https://www.youtube.com/watch?v=6LDgLWCQSSM"},
 | 
			
		||||
            {"name": "MC ENDRAY - AUDI", "url": "https://www.youtube.com/watch?v=oIjNoMGEuRg"},
 | 
			
		||||
            {"name": "Mercy Dance - I Pajuri", "url": "https://www.youtube.com/watch?v=RPpkMh47l9w"},
 | 
			
		||||
            {"name": "NL - Pasitusinam", "url": "https://www.youtube.com/watch?v=WhSFudvloog"},
 | 
			
		||||
            {"name": "SixthBoi - Nevaidink", "url": "https://www.youtube.com/watch?v=nOTNnnrqTII"},
 | 
			
		||||
            {"name": "Mr.Bullet - UZ MUS IR JUS", "url": "https://www.youtube.com/watch?v=85q_7jXEgH8"},
 | 
			
		||||
            {"name": "Jovani, Karaliska Erdve - Is Leto Leidziasi Saule", "url": "https://www.youtube.com/watch?v=VqSu8iG1_DE"},
 | 
			
		||||
            {"name": "Rondo - Margarita", "url": "https://www.youtube.com/watch?v=rF4w-Rxsiv4"},
 | 
			
		||||
            {"name": "Radvis - TU ESI MELAGIS (Techno Extended)", "url": "https://www.youtube.com/watch?v=kmvvP7GW_bw"},
 | 
			
		||||
            {"name": "Zas - Myliu kina", "url": "https://www.youtube.com/watch?v=ImFrfmi-qT8"},
 | 
			
		||||
            {"name": "Zilvinas Zvagulis - Amerikonas grizo sunus", "url": "https://www.youtube.com/watch?v=UvzJEz5ADY8"},
 | 
			
		||||
            {"name": "Raketa - I Kluba", "url": "https://www.youtube.com/watch?v=FkSjtpYN3EI"},
 | 
			
		||||
            {"name": "Karaliska Erdve - Vakareja", "url": "https://www.youtube.com/watch?v=g0HmrlJ7fhE"},
 | 
			
		||||
            {"name": "Tnn - Parukom", "url": "https://www.youtube.com/watch?v=v9pBZK2RIPI"},
 | 
			
		||||
            {"name": "DJ Dalgis - Kauniete", "url": "https://www.youtube.com/watch?v=b3xPE9Iyuzc"},
 | 
			
		||||
            {"name": "Andzikas - I gamta", "url": "https://www.youtube.com/watch?v=UyLdjC-hihM"},
 | 
			
		||||
            {"name": "nemuno krantai - rytmecio rasos", "url": "https://www.youtube.com/watch?v=2-fGbsrofv4"},
 | 
			
		||||
            {"name": "Tipo grupe - Lovoj Vezi", "url": "https://www.youtube.com/watch?v=M3zVMzWCy_c"},
 | 
			
		||||
            {"name": "Kastanenda - Sombrero", "url": "https://www.youtube.com/watch?v=3Z3_4TknCfQ"},
 | 
			
		||||
            {"name": "Elektra - Juda Tavo rankos", "url": "https://www.youtube.com/watch?v=k2RuDoudnOE"},
 | 
			
		||||
            {"name": "Vilija ir Marijonas mikutavicius - Dabar Geriausi Musu Vakarai", "url": "https://www.youtube.com/watch?v=MPnZkEscWo0"},
 | 
			
		||||
            {"name": "Parnesk alaus OG", "url": "https://www.youtube.com/watch?v=e7cB1JIlZ2k"},
 | 
			
		||||
            {"name": "Eugenijus Ostapenko - Dviratukas", "url": "https://www.youtube.com/watch?v=ILFHZQK33Mw"},
 | 
			
		||||
            {"name": "Ciulpuoneliai - Jau Nutilo Sirgaliai", "url": "https://www.youtube.com/watch?v=s8qIVA1U0C0"},
 | 
			
		||||
            {"name": "Tweaxx - Mersas", "url": "https://www.youtube.com/watch?v=7ljAzgALPdA"},
 | 
			
		||||
            {"name": "Dove - Naktinis Tusas", "url": "https://www.youtube.com/watch?v=pz-HEAwFEnk"},
 | 
			
		||||
            {"name": "MAMA MANE RODYS PER FARUS", "url": "https://www.youtube.com/watch?v=F5HqXYRDZaE"},
 | 
			
		||||
            {"name": "Kastaneda - Kelyje", "url": "https://www.youtube.com/watch?v=JVE6NQqKPL4"},
 | 
			
		||||
            {"name": "NL - Juodas Golfas", "url": "https://www.youtube.com/watch?v=f2-ZmElSvPc"},
 | 
			
		||||
            {"name": "DJ Dalgis - Zalia Siera", "url": "https://www.youtube.com/watch?v=nfentq_pez4"},
 | 
			
		||||
            {"name": "L1GHT CASH - Whiskey Cola Lietuviskai (sultys degtinele) remix", "url": "https://www.youtube.com/watch?v=YVaqDaf1KXU"},
 | 
			
		||||
            {"name": "Tipo grupe ir Kastaneda - Po stikliuka", "url": "https://www.youtube.com/watch?v=EtmE60nE7fI"},
 | 
			
		||||
            {"name": "MG INTERNATIONAL - JUODA ORCHIDEJA", "url": "https://www.youtube.com/watch?v=HQvceFRBq9M"},
 | 
			
		||||
            {"name": "Ganja - Truputi", "url": "https://www.youtube.com/watch?v=Pxve7CwiCHM"},
 | 
			
		||||
            {"name": "Riaukenzo - Trys Trys Trys", "url": "https://www.youtube.com/watch?v=qJv6GRQCnCk"},
 | 
			
		||||
            {"name": "Grupiokai - Degtine", "url": "https://www.youtube.com/watch?v=8SqbG2VmEFw"},
 | 
			
		||||
            {"name": "Robertas Kupstas - Cia Mano Rojus", "url": "https://www.youtube.com/watch?v=xij_YeEInr8"},
 | 
			
		||||
            {"name": "NIERKA - PENKTADIENIS", "url": "https://www.youtube.com/watch?v=h3TuZj_OAf0"},
 | 
			
		||||
            {"name": "VAIKAI PO LELIJOM (REMIX)", "url": "https://www.youtube.com/watch?v=k1amBbsAZuo"},
 | 
			
		||||
            {"name": "Vitalija Katunskyte - Robinzonas", "url": "https://www.youtube.com/watch?v=erDHG-QpbPY"},
 | 
			
		||||
            {"name": "Rycka klipas", "url": "https://www.youtube.com/watch?v=nuTUDSQ3BBI"},
 | 
			
		||||
            {"name": "Nezinau, Kodel...", "url": "https://www.youtube.com/watch?v=A-i2CkCnPoc"},
 | 
			
		||||
            {"name": "NL - R1", "url": "https://www.youtube.com/watch?v=hSgav4fYnZ8"},
 | 
			
		||||
            {"name": "DJ Dalgis - Negeriau", "url": "https://www.youtube.com/watch?v=c89YvG3MCcs"},
 | 
			
		||||
            {"name": "Tipo Grupe - tipo daina", "url": "https://www.youtube.com/watch?v=PTIOaSjEgIU"},
 | 
			
		||||
            {"name": "Depresinis feat. Deivas - 0,7", "url": "https://www.youtube.com/watch?v=rjwFjBgTzAA"},
 | 
			
		||||
            {"name": "Depresinis & MERAKI2004 - VASARA ZJBS", "url": "https://www.youtube.com/watch?v=BD-pBjRy-5A"},
 | 
			
		||||
            {"name": "Depresinis feat. Deivas - LEDUKAI", "url": "https://www.youtube.com/watch?v=R2-MtpkKgGI"},
 | 
			
		||||
            {"name": "Depresinis feat. Deivas - Pavasaris", "url": "https://www.youtube.com/watch?v=yWWAucfQdN4"},
 | 
			
		||||
            {"name": "Depresinis - LEDINE", "url": "https://www.youtube.com/watch?v=qugvChkXMLk"},
 | 
			
		||||
            {"name": "Depresinis, Jypas - O Mazuti", "url": "https://www.youtube.com/watch?v=4t_DPbe2r3M"},
 | 
			
		||||
            {"name": "AVA - Eik Tu NA", "url": "https://www.youtube.com/watch?v=yRf3ijaIgOg"},
 | 
			
		||||
            {"name": "Judam Lietuvoj", "url": "https://www.youtube.com/watch?v=WDzWSEgSy5U"},
 | 
			
		||||
            {"name": "16Hz - Baliavojam", "url": "https://www.youtube.com/watch?v=Ia-qERX8WLs"},
 | 
			
		||||
            {"name": "Deivas - Klaipeda On Top", "url": "https://www.youtube.com/watch?v=g_h2M3e2OYU"},
 | 
			
		||||
            {"name": "Depresinis - Volkswagina", "url": "https://www.youtube.com/watch?v=1lZR1VKsQHo"},
 | 
			
		||||
            {"name": "SADBOY - Kaifuok", "url": "https://www.youtube.com/watch?v=vclryWgfy8I"},
 | 
			
		||||
            {"name": "SADBOY - Blizgantys Naikai", "url": "https://www.youtube.com/watch?v=p5KsYJGcfOM"},
 | 
			
		||||
            {"name": "SADBOY - 1001 Naktis", "url": "https://www.youtube.com/watch?v=mLJIjGvWmKI"},
 | 
			
		||||
            {"name": "SADBOY - Deginam", "url": "https://www.youtube.com/watch?v=w3R0Aq1EGXg"},
 | 
			
		||||
            {"name": "Wenona Waves - Topine Panele", "url": "https://www.youtube.com/watch?v=MPHuhmUomfE"},
 | 
			
		||||
            {"name": "Andzikas - Virs debesu", "url": "https://www.youtube.com/watch?v=PHJcVGhxra8"},
 | 
			
		||||
            {"name": "Grupe MX - 1.9 TDI", "url": "https://www.youtube.com/watch?v=8FBr5GQXsI8"},
 | 
			
		||||
            {"name": "Patruliai - Kur Tu", "url": "https://www.youtube.com/watch?v=OPWhiu3cvj0"},
 | 
			
		||||
            {"name": "Ka Tu Ka Vakare", "url": "https://www.youtube.com/watch?v=6SOS4ljHbJY"}
 | 
			
		||||
        ],
 | 
			
		||||
        "lietuviskos/rave": [
 | 
			
		||||
            {"name": "VainHouse - Malunas Prie Kelio", "url": "https://www.youtube.com/watch?v=bbwuNjDXCiM"},
 | 
			
		||||
            {"name": "Sokoledas - Mano Skonis Sokolado (Matuze & Arnisxd Remix)", "url": "https://www.youtube.com/watch?v=hb41bfQxiM0"}
 | 
			
		||||
        ],
 | 
			
		||||
        "rusiskos": [
 | 
			
		||||
            {"name": "Topolini puh", "url": "https://www.youtube.com/watch?v=UUryvYF8tUs"},
 | 
			
		||||
            {"name": "Raim & Artur feat. Zhenis - Diskoteka is 90 hit", "url": "https://www.youtube.com/watch?v=GfBhxlNhrn0"},
 | 
			
		||||
            {"name": "Pimp Schwab - vse shto nas ne Ubivaet", "url": "https://www.youtube.com/watch?v=NTEXFyUE9Ww"},
 | 
			
		||||
            {"name": "Dzaro and hansa - Visky Kola karaleva trans pola", "url": "https://www.youtube.com/watch?v=fflrMvZ2HtA"}
 | 
			
		||||
        ],
 | 
			
		||||
        "noclue": [
 | 
			
		||||
            {"name": "Bad Boys", "url": "https://www.youtube.com/watch?v=NTC7RD8xzCY"},
 | 
			
		||||
            {"name": "DR. VODKA - DZIEWCZYNO Z TIKTOKA", "url": "https://www.youtube.com/watch?v=HLbw1WQt64o"},
 | 
			
		||||
            {"name": "Maco Mamuko - Whiskey, Cola i Tequila", "url": "https://www.youtube.com/watch?v=aBrN0k0Phtc"}
 | 
			
		||||
    
 | 
			
		||||
        ],
 | 
			
		||||
        "reggea": [
 | 
			
		||||
            {"name": "Shaggy - It Wasn't Me", "url": "https://www.youtube.com/watch?v=ssVj50ombaM"}
 | 
			
		||||
        ],
 | 
			
		||||
        "alt": [
 | 
			
		||||
            {"name": "ROMANCEPLANET - FALL FROM THE SKY", "url": "https://www.youtube.com/watch?v=HMhzxzXBisw"},
 | 
			
		||||
            {"name": "ROMANCEPLANET - PLAIN WHITE TEE", "url": "https://www.youtube.com/watch?v=tdVQbNwjGac"},
 | 
			
		||||
            {"name": "ROMANCEPLANET - DANCE", "url": "https://www.youtube.com/watch?v=ircOfMb4gEw"}
 | 
			
		||||
        ]
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,46 +1,46 @@
 | 
			
		|||
use crate::{config::ConfigWrapper, downloader::Downloader, manifest::{Manifest, ManifestSong}};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
pub async fn add(cfg: &ConfigWrapper, manifest: &mut Manifest, downloader: &mut Downloader, url: &Option<String>, name: &Option<String>, genre: &Option<String>) -> anyhow::Result<()> {
 | 
			
		||||
    
 | 
			
		||||
    log::debug!("Genre: {genre:?}");
 | 
			
		||||
    log::debug!("url: {url:?}");
 | 
			
		||||
    log::debug!("name: {name:?}");
 | 
			
		||||
 | 
			
		||||
    let mut genres = manifest.genres.keys().map(|f| f.clone()).collect::<Vec<String>>();
 | 
			
		||||
 | 
			
		||||
    genres.sort();
 | 
			
		||||
 | 
			
		||||
    let genre = genre.clone().unwrap_or_else( || {
 | 
			
		||||
        let g = crate::prompt::prompt_with_list_or_str("Enter song genre", &genres);
 | 
			
		||||
        log::info!("Genre: {g}");
 | 
			
		||||
        g
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    let url = url.clone().unwrap_or_else( ||
 | 
			
		||||
        crate::prompt::simple_prompt("Enter song youtube url, make sure its not a playlist, (yt only for now)")
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let name = name.clone().unwrap_or_else( ||
 | 
			
		||||
        crate::prompt::simple_prompt("Enter song name with like this: {Author} - {Song name}")
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    manifest.add_song(genre.clone(), name.clone(), url.clone())?;
 | 
			
		||||
    manifest.save()?;
 | 
			
		||||
 | 
			
		||||
    let should_download = crate::prompt::prompt_bool("Download song now?", Some(false));
 | 
			
		||||
 | 
			
		||||
    if should_download {
 | 
			
		||||
        let song = &ManifestSong {
 | 
			
		||||
            name,
 | 
			
		||||
            url,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        downloader.download_song(cfg, song, &genre, &manifest.format()?).await?;
 | 
			
		||||
        downloader.wait_for_procs(0).await?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
use crate::{config::ConfigWrapper, downloader::Downloader, manifest::{Manifest, ManifestSong}};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
pub async fn add(cfg: &ConfigWrapper, manifest: &mut Manifest, downloader: &mut Downloader, url: &Option<String>, name: &Option<String>, genre: &Option<String>) -> anyhow::Result<()> {
 | 
			
		||||
    
 | 
			
		||||
    log::debug!("Genre: {genre:?}");
 | 
			
		||||
    log::debug!("url: {url:?}");
 | 
			
		||||
    log::debug!("name: {name:?}");
 | 
			
		||||
 | 
			
		||||
    let mut genres = manifest.genres.keys().map(|f| f.clone()).collect::<Vec<String>>();
 | 
			
		||||
 | 
			
		||||
    genres.sort();
 | 
			
		||||
 | 
			
		||||
    let genre = genre.clone().unwrap_or_else( || {
 | 
			
		||||
        let g = crate::prompt::prompt_with_list_or_str("Enter song genre", &genres);
 | 
			
		||||
        log::info!("Genre: {g}");
 | 
			
		||||
        g
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    let url = url.clone().unwrap_or_else( ||
 | 
			
		||||
        crate::prompt::simple_prompt("Enter song youtube url, make sure its not a playlist, (yt only for now)")
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let name = name.clone().unwrap_or_else( ||
 | 
			
		||||
        crate::prompt::simple_prompt("Enter song name with like this: {Author} - {Song name}")
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    manifest.add_song(genre.clone(), name.clone(), url.clone())?;
 | 
			
		||||
    manifest.save()?;
 | 
			
		||||
 | 
			
		||||
    let should_download = crate::prompt::prompt_bool("Download song now?", Some(false));
 | 
			
		||||
 | 
			
		||||
    if should_download {
 | 
			
		||||
        let song = &ManifestSong {
 | 
			
		||||
            name,
 | 
			
		||||
            url,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        downloader.download_song(cfg, song, &genre, &manifest.format()?).await?;
 | 
			
		||||
        downloader.wait_for_procs(0).await?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,27 +1,28 @@
 | 
			
		|||
mod add;
 | 
			
		||||
 | 
			
		||||
use crate::{config::{cli::CliCommand, ConfigWrapper}, downloader::Downloader, manifest::Manifest};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
pub async fn command_run(cfg: &ConfigWrapper, manifest: &mut Manifest) -> anyhow::Result<()> {
 | 
			
		||||
    let mut downloader = Downloader::new(cfg.cfg.ytdlp.path.clone());
 | 
			
		||||
    match &cfg.cli.command {
 | 
			
		||||
        None | Some(CliCommand::Download) => {
 | 
			
		||||
            if let Ok(count) = downloader.download_all(manifest, &cfg).await {
 | 
			
		||||
                log::info!("Downloaded {count} songs");
 | 
			
		||||
            } else {
 | 
			
		||||
                log::error!("Failed to download songs");
 | 
			
		||||
                return Ok(());
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        Some(c) => {
 | 
			
		||||
            match c {
 | 
			
		||||
                CliCommand::Download => unreachable!(),
 | 
			
		||||
                CliCommand::Add { url, name, genre  } => add::add(cfg, manifest, &mut downloader, url, name, genre).await?,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
mod add;
 | 
			
		||||
 | 
			
		||||
use crate::{config::{cli::CliCommand, ConfigWrapper}, downloader::Downloader, manifest::Manifest};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
pub async fn command_run(cfg: &ConfigWrapper, manifest: &mut Manifest) -> anyhow::Result<()> {
 | 
			
		||||
    let mut downloader = Downloader::new();
 | 
			
		||||
    match &cfg.cli.command {
 | 
			
		||||
        None | Some(CliCommand::Download) => {
 | 
			
		||||
            match downloader.download_all(manifest, &cfg).await {
 | 
			
		||||
                Ok(count) => log::info!("Downloaded {count} songs"),
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    log::error!("Failed to download songs: {e}");
 | 
			
		||||
                    return Ok(());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        Some(c) => {
 | 
			
		||||
            match c {
 | 
			
		||||
                CliCommand::Download => unreachable!(),
 | 
			
		||||
                CliCommand::Add { url, name, genre  } => add::add(cfg, manifest, &mut downloader, url, name, genre).await?,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,41 +1,40 @@
 | 
			
		|||
use camino::Utf8PathBuf;
 | 
			
		||||
use clap::{Parser, Subcommand};
 | 
			
		||||
 | 
			
		||||
use crate::util::isatty;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Parser, Default)]
 | 
			
		||||
pub struct CliArgs {
 | 
			
		||||
    /// Show more info
 | 
			
		||||
    #[arg(long, short)]
 | 
			
		||||
    pub debug: bool,
 | 
			
		||||
 | 
			
		||||
    /// Path to manifest
 | 
			
		||||
    #[arg(long, short, default_value_t=Utf8PathBuf::from("./manifest.json"))]
 | 
			
		||||
    pub manifest: Utf8PathBuf,
 | 
			
		||||
 | 
			
		||||
    /// Output directory
 | 
			
		||||
    #[arg(long, short, default_value_t=Utf8PathBuf::from("./out"))]
 | 
			
		||||
    pub output: Utf8PathBuf,
 | 
			
		||||
 | 
			
		||||
    /// Config path
 | 
			
		||||
    #[arg(long, short, default_value_t=Utf8PathBuf::from("./config.json"))]
 | 
			
		||||
    pub config: Utf8PathBuf,
 | 
			
		||||
 | 
			
		||||
    #[command(subcommand)]
 | 
			
		||||
    pub command: Option<CliCommand>,
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Subcommand, Default)]
 | 
			
		||||
pub enum CliCommand {
 | 
			
		||||
    #[default]
 | 
			
		||||
    Download,
 | 
			
		||||
    Add {
 | 
			
		||||
        #[arg(long, short)]
 | 
			
		||||
        url: Option<String>,
 | 
			
		||||
        #[arg(long, short)]
 | 
			
		||||
        name: Option<String>,
 | 
			
		||||
        #[arg(long, short)]
 | 
			
		||||
        genre: Option<String>
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
use camino::Utf8PathBuf;
 | 
			
		||||
use clap::{Parser, Subcommand};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Parser, Default)]
 | 
			
		||||
pub struct CliArgs {
 | 
			
		||||
    /// Show more info
 | 
			
		||||
    #[arg(long, short)]
 | 
			
		||||
    pub debug: bool,
 | 
			
		||||
 | 
			
		||||
    /// Path to manifest
 | 
			
		||||
    #[arg(long, short, default_value_t=Utf8PathBuf::from("./manifest.json"))]
 | 
			
		||||
    pub manifest: Utf8PathBuf,
 | 
			
		||||
 | 
			
		||||
    /// Output directory
 | 
			
		||||
    #[arg(long, short, default_value_t=Utf8PathBuf::from("./out"))]
 | 
			
		||||
    pub output: Utf8PathBuf,
 | 
			
		||||
 | 
			
		||||
    /// Config path
 | 
			
		||||
    #[arg(long, short, default_value_t=Utf8PathBuf::from("./config.json"))]
 | 
			
		||||
    pub config: Utf8PathBuf,
 | 
			
		||||
 | 
			
		||||
    #[command(subcommand)]
 | 
			
		||||
    pub command: Option<CliCommand>,
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Subcommand, Default)]
 | 
			
		||||
pub enum CliCommand {
 | 
			
		||||
    #[default]
 | 
			
		||||
    Download,
 | 
			
		||||
    Add {
 | 
			
		||||
        #[arg(long, short)]
 | 
			
		||||
        url: Option<String>,
 | 
			
		||||
        #[arg(long, short)]
 | 
			
		||||
        name: Option<String>,
 | 
			
		||||
        #[arg(long, short)]
 | 
			
		||||
        genre: Option<String>
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,141 +1,133 @@
 | 
			
		|||
pub mod cli;
 | 
			
		||||
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
use clap::Parser;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use crate::util::{self, dl_to_file, isatty};
 | 
			
		||||
 | 
			
		||||
use self::cli::CliArgs;
 | 
			
		||||
 | 
			
		||||
const YTDLP_DL_URL: &'static str = "https://github.com/yt-dlp/yt-dlp/archive/refs/heads/master.zip";
 | 
			
		||||
const SPOTDL_DL_URL: &'static str = "https://github.com/spotDL/spotify-downloader/archive/refs/heads/master.zip";
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Default)]
 | 
			
		||||
pub struct ConfigWrapper {
 | 
			
		||||
    pub cfg: Config,
 | 
			
		||||
    pub cli: cli::CliArgs,
 | 
			
		||||
    pub isatty: bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize, Default)]
 | 
			
		||||
pub struct Config {
 | 
			
		||||
    pub ytdlp: ConfigYtdlp,
 | 
			
		||||
    pub spotdl: ConfigSpotdl,
 | 
			
		||||
    pub python: ConfigPython,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize, Default)]
 | 
			
		||||
pub struct ConfigYtdlp {
 | 
			
		||||
    pub path: PathBuf,
 | 
			
		||||
    pub is_python: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize, Default)]
 | 
			
		||||
pub struct ConfigSpotdl {
 | 
			
		||||
    pub path: PathBuf,
 | 
			
		||||
    pub is_python: bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize, Default)]
 | 
			
		||||
pub struct ConfigPython {
 | 
			
		||||
    pub path: PathBuf,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
impl ConfigWrapper {
 | 
			
		||||
    pub async fn parse() -> Result<Self> {
 | 
			
		||||
        let mut s = Self::default();
 | 
			
		||||
        s.cli = cli::CliArgs::parse();
 | 
			
		||||
        crate::logger::init_logger(s.cli.debug);
 | 
			
		||||
        s.cfg = Config::parse(&s.cli).await?;
 | 
			
		||||
        s.isatty = isatty();
 | 
			
		||||
        Ok(s)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Config {
 | 
			
		||||
    pub async fn parse(cli: &CliArgs) -> Result<Self> {
 | 
			
		||||
        if !cli.config.exists() {
 | 
			
		||||
            log::info!("Config doesnt exist");
 | 
			
		||||
            return Self::setup_config(&cli).await;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let data = std::fs::read_to_string(&cli.config)?;
 | 
			
		||||
        let data: Self = serde_json::from_str(&data)?;
 | 
			
		||||
        Ok(data)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn setup_config(cli: &CliArgs) -> Result<Self> {
 | 
			
		||||
        let mut s = Self::default();
 | 
			
		||||
 | 
			
		||||
        let bin_dir = cli.output.clone().into_std_path_buf().join(".bin/");
 | 
			
		||||
        let mut python_needed = false;
 | 
			
		||||
 | 
			
		||||
        match util::is_program_in_path("yt-dlp") {
 | 
			
		||||
            Some(p) => {
 | 
			
		||||
                s.ytdlp.path = p;
 | 
			
		||||
                s.ytdlp.is_python = false;
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            None => {
 | 
			
		||||
                python_needed = true;
 | 
			
		||||
                s.ytdlp.is_python = true;
 | 
			
		||||
                s.ytdlp.path = bin_dir.join("ytdlp");
 | 
			
		||||
                dl_to_file(YTDLP_DL_URL, s.ytdlp.path.join("ytdlp.zip")).await?;
 | 
			
		||||
                zip_extensions::zip_extract(&s.ytdlp.path.join("ytdlp.zip"), &s.ytdlp.path)?;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        match util::is_program_in_path("spotdl") {
 | 
			
		||||
            Some(p) => {
 | 
			
		||||
                s.spotdl.path = p;
 | 
			
		||||
                s.spotdl.is_python = false;
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            None => {
 | 
			
		||||
                python_needed = true;
 | 
			
		||||
                s.spotdl.is_python = true;
 | 
			
		||||
                s.spotdl.path = bin_dir.join("ytdlp");
 | 
			
		||||
                dl_to_file(SPOTDL_DL_URL, s.spotdl.path.join("spotdl.zip")).await?;
 | 
			
		||||
                zip_extensions::zip_extract(&s.spotdl.path.join("spotdl.zip"), &s.ytdlp.path)?;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        let python_paths = &[
 | 
			
		||||
            util::is_program_in_path("python"),
 | 
			
		||||
            util::is_program_in_path("python3")
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        if python_needed {
 | 
			
		||||
            let mut found = false;
 | 
			
		||||
            for p in python_paths {
 | 
			
		||||
                match p {
 | 
			
		||||
                    Some(p) => {
 | 
			
		||||
                        s.python.path = p.clone();
 | 
			
		||||
                        found = true;
 | 
			
		||||
                        break
 | 
			
		||||
                    }
 | 
			
		||||
                    None => {
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if !found {
 | 
			
		||||
                panic!("Python needs to be installed for this to work, or install ytdlp and spotdl manually, (dont forget to delete the config file after doing so)");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        s.save(cli.config.clone().into_std_path_buf())?;
 | 
			
		||||
        Ok(s)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn save(&self, path: PathBuf) -> anyhow::Result<()> {
 | 
			
		||||
        let data = serde_json::to_string_pretty(self)?;
 | 
			
		||||
        std::fs::write(path, data)?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
pub mod cli;
 | 
			
		||||
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
use clap::Parser;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use crate::util::{self, isatty};
 | 
			
		||||
 | 
			
		||||
use self::cli::CliArgs;
 | 
			
		||||
 | 
			
		||||
// const YTDLP_DL_URL: &'static str = "https://github.com/yt-dlp/yt-dlp/archive/refs/heads/master.zip";
 | 
			
		||||
// const SPOTDL_DL_URL: &'static str = "https://github.com/spotDL/spotify-downloader/archive/refs/heads/master.zip";
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Default)]
 | 
			
		||||
pub struct ConfigWrapper {
 | 
			
		||||
    pub cfg: Config,
 | 
			
		||||
    pub cli: cli::CliArgs,
 | 
			
		||||
    pub isatty: bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize, Default)]
 | 
			
		||||
pub struct Config {
 | 
			
		||||
    pub ytdlp: ConfigYtdlp,
 | 
			
		||||
    pub spotdl: ConfigSpotdl,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize, Default)]
 | 
			
		||||
pub struct ConfigYtdlp {
 | 
			
		||||
    pub path: PathBuf,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Serialize, Deserialize, Default)]
 | 
			
		||||
pub struct ConfigSpotdl {
 | 
			
		||||
    pub path: PathBuf,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
impl ConfigWrapper {
 | 
			
		||||
    pub async fn parse() -> Result<Self> {
 | 
			
		||||
        let mut s = Self::default();
 | 
			
		||||
        s.cli = cli::CliArgs::parse();
 | 
			
		||||
        crate::logger::init_logger(s.cli.debug);
 | 
			
		||||
        s.cfg = Config::parse(&s.cli).await?;
 | 
			
		||||
        s.isatty = isatty();
 | 
			
		||||
        Ok(s)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Config {
 | 
			
		||||
    pub async fn parse(cli: &CliArgs) -> Result<Self> {
 | 
			
		||||
        if !cli.config.exists() {
 | 
			
		||||
            log::info!("Config doesnt exist");
 | 
			
		||||
            return Self::setup_config(&cli).await;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let data = std::fs::read_to_string(&cli.config)?;
 | 
			
		||||
        let data: Self = serde_json::from_str(&data)?;
 | 
			
		||||
        Ok(data)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn setup_config(cli: &CliArgs) -> Result<Self> {
 | 
			
		||||
        let mut s = Self::default();
 | 
			
		||||
        let mut error = false;
 | 
			
		||||
 | 
			
		||||
        match util::is_program_in_path("yt-dlp") {
 | 
			
		||||
            Some(p) => {
 | 
			
		||||
                s.ytdlp.path = p;
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            None => {
 | 
			
		||||
                error = true;
 | 
			
		||||
                log::error!("could not find yt-dlp, please install it.");
 | 
			
		||||
                log::info!(" - With winget (Windows only) (recommended):");
 | 
			
		||||
                log::info!("   - Most new windows versions have winget installed, if not, instructions here: https://learn.microsoft.com/en-us/windows/package-manager/winget/#install-winget");
 | 
			
		||||
                log::info!("   - run `winget install yt-dlp`");
 | 
			
		||||
                log::info!(" - With chocolatey (Windows only):");
 | 
			
		||||
                log::info!("   - Make sure you have chocolatey installed - https://chocolatey.org/install");
 | 
			
		||||
                log::info!("   - run `choco install yt-dlp` as Admin");
 | 
			
		||||
                log::info!(" - With pip (from python) (Cross platform)");
 | 
			
		||||
                log::info!("   - Make sure you have python installed");
 | 
			
		||||
                log::info!("   - pip install yt-dlp");
 | 
			
		||||
                log::info!(" - Using your distro's package manager (Unix/BSD only) (Not recommended)")
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        match util::is_program_in_path("spotdl") {
 | 
			
		||||
            Some(p) => {
 | 
			
		||||
                s.spotdl.path = p;
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            None => {
 | 
			
		||||
                let res = crate::prompt::prompt_bool("Spotdl is not installed but if you dont need to download music from spotify you dont need it, skip it?", None);
 | 
			
		||||
                if res {
 | 
			
		||||
                    s.spotdl.path = PathBuf::from("UNUSED");
 | 
			
		||||
                } else {
 | 
			
		||||
                    error = true;
 | 
			
		||||
                    log::error!("could not find spotdl, please install it. ");
 | 
			
		||||
                    log::info!(" - With pip (from python) (Cross platform) (recommended)");
 | 
			
		||||
                    log::info!("   - Make sure you have python installed - https://www.python.org/downloads/");
 | 
			
		||||
                    log::info!("   - pip install spotdl");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        match util::is_program_in_path("ffmpeg") {
 | 
			
		||||
            Some(_) => (),
 | 
			
		||||
 | 
			
		||||
            None => {
 | 
			
		||||
                error = true;
 | 
			
		||||
                log::error!("could not find ffmpeg, please install it.");
 | 
			
		||||
                log::info!(" - With winget (Windows only) (recommended):");
 | 
			
		||||
                log::info!("   - Most new windows versions have winget installed, if not, instructions here: https://learn.microsoft.com/en-us/windows/package-manager/winget/#install-winget");
 | 
			
		||||
                log::info!("   - run `winget install --id=Gyan.FFmpeg -e`");
 | 
			
		||||
                log::info!(" - With chocolatey (Windows only):");
 | 
			
		||||
                log::info!("   - Make sure you have chocolatey installed - https://chocolatey.org/install");
 | 
			
		||||
                log::info!("   - run `choco install ffmpeg` as Admin");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if !error {
 | 
			
		||||
            s.save(cli.config.clone().into_std_path_buf())?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(s)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn save(&self, path: PathBuf) -> anyhow::Result<()> {
 | 
			
		||||
        let data = serde_json::to_string_pretty(self)?;
 | 
			
		||||
        std::fs::write(path, data)?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,15 @@
 | 
			
		|||
 | 
			
		||||
#[cfg(target_family="windows")]
 | 
			
		||||
mod constants {
 | 
			
		||||
    pub const PATH_VAR_SEP: &'static str = ";";
 | 
			
		||||
    pub const EXEC_EXT: &'static str = "exe";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(target_family="unix")]
 | 
			
		||||
mod constants {
 | 
			
		||||
    pub const PATH_VAR_SEP: &'static str = ":";
 | 
			
		||||
    pub const EXEC_EXT: &'static str = "";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#[cfg(target_family="windows")]
 | 
			
		||||
mod constants {
 | 
			
		||||
    pub const PATH_VAR_SEP: &'static str = ";";
 | 
			
		||||
    pub const EXEC_EXT: &'static str = "exe";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(target_family="unix")]
 | 
			
		||||
mod constants {
 | 
			
		||||
    pub const PATH_VAR_SEP: &'static str = ":";
 | 
			
		||||
    pub const EXEC_EXT: &'static str = "";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
pub use constants::*;
 | 
			
		||||
| 
						 | 
				
			
			@ -1,117 +1,132 @@
 | 
			
		|||
use std::{collections::HashMap, path::PathBuf, process::Stdio};
 | 
			
		||||
 | 
			
		||||
use lazy_static::lazy_static;
 | 
			
		||||
use log::Level;
 | 
			
		||||
use tokio::sync::{Mutex, RwLock};
 | 
			
		||||
 | 
			
		||||
use crate::{config::ConfigWrapper, manifest::{Manifest, ManifestSong}};
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
struct Proc {
 | 
			
		||||
    url: String,
 | 
			
		||||
    path: String,
 | 
			
		||||
    finished: bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
lazy_static!(
 | 
			
		||||
    static ref PROCESSES: Mutex<RwLock<HashMap<usize, Proc>>> = Mutex::new(RwLock::new(HashMap::new()));
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
pub struct Downloader {
 | 
			
		||||
    count: usize,
 | 
			
		||||
    ytdlp_path: PathBuf,
 | 
			
		||||
    id_itr: usize,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Downloader {
 | 
			
		||||
    pub fn new(ytdlp_path: PathBuf) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            count: 0,
 | 
			
		||||
            ytdlp_path,
 | 
			
		||||
            id_itr: 0,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn download_all(&mut self, manifest: &Manifest, cfg: &ConfigWrapper) -> anyhow::Result<usize> {
 | 
			
		||||
        let format = manifest.format()?;
 | 
			
		||||
 | 
			
		||||
        for (genre, songs) in &manifest.genres {
 | 
			
		||||
            for song in songs {
 | 
			
		||||
                self.download_song(cfg, &song, &genre, &format).await?;
 | 
			
		||||
                self.wait_for_procs(10).await?;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        self.wait_for_procs(0).await?;
 | 
			
		||||
        Ok(self.count)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    pub async fn download_song(&mut self, cfg: &ConfigWrapper, song: &ManifestSong, genre: &String, format: &String) -> anyhow::Result<()> {
 | 
			
		||||
        let path = format!("{}/{genre}/{}.{}", cfg.cli.output, song.name, &format);
 | 
			
		||||
 | 
			
		||||
        if PathBuf::from(&path).exists() {
 | 
			
		||||
            log::debug!("File {path} exists, skipping");
 | 
			
		||||
            return Ok(())
 | 
			
		||||
        }
 | 
			
		||||
        let mut cmd = tokio::process::Command::new(&self.ytdlp_path);
 | 
			
		||||
        let cmd = cmd.args([
 | 
			
		||||
                "-x",
 | 
			
		||||
                "--audio-format",
 | 
			
		||||
                format.as_str(),
 | 
			
		||||
                "-o",
 | 
			
		||||
                path.as_str(),
 | 
			
		||||
                song.url.as_str()
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
        let cmd = if log::max_level() < Level::Debug {
 | 
			
		||||
            cmd.stdout(Stdio::null()).stderr(Stdio::null())
 | 
			
		||||
        } else {cmd};
 | 
			
		||||
 | 
			
		||||
        let mut proc = cmd.spawn()?;
 | 
			
		||||
        let id = self.id_itr;
 | 
			
		||||
        
 | 
			
		||||
        tokio::spawn(async move {
 | 
			
		||||
            let id = id;
 | 
			
		||||
            proc.wait().await
 | 
			
		||||
                .expect("child process encountered an error");
 | 
			
		||||
            PROCESSES.lock().await.write().await.get_mut(&id).unwrap().finished = true;
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        log::info!("Downloading {path}");
 | 
			
		||||
        PROCESSES.lock().await.write().await.insert(id, Proc {
 | 
			
		||||
            url: song.url.clone(),
 | 
			
		||||
            path,
 | 
			
		||||
            finished: false,
 | 
			
		||||
        });
 | 
			
		||||
        self.id_itr += 1;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn wait_for_procs(&mut self, until: usize) -> anyhow::Result<()> {
 | 
			
		||||
        // NOTE: This looks really fucked because i dont want to deadlock the processes so i lock PROCESSES for as little as possible
 | 
			
		||||
        // NOTE: So its also kinda really slow
 | 
			
		||||
        loop {
 | 
			
		||||
            {
 | 
			
		||||
                if PROCESSES.lock().await.read().await.len() <= until {
 | 
			
		||||
                    return Ok(());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let procs = {
 | 
			
		||||
                PROCESSES.lock().await.read().await.clone()
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            for (idx, proc) in procs {
 | 
			
		||||
                if proc.finished {
 | 
			
		||||
                    {
 | 
			
		||||
                        PROCESSES.lock().await.write().await.remove(&idx);
 | 
			
		||||
                    }
 | 
			
		||||
                    log::info!("Finished downloading {}", proc.path);
 | 
			
		||||
                    self.count += 1;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        #[allow(unreachable_code)] //? rust_analizer not smart enough for this
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
use std::{collections::HashMap, path::PathBuf, process::Stdio};
 | 
			
		||||
 | 
			
		||||
use lazy_static::lazy_static;
 | 
			
		||||
use log::Level;
 | 
			
		||||
use tokio::sync::{Mutex, RwLock};
 | 
			
		||||
 | 
			
		||||
use crate::{config::ConfigWrapper, manifest::{Manifest, ManifestSong}};
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
struct Proc {
 | 
			
		||||
    url: String,
 | 
			
		||||
    path: String,
 | 
			
		||||
    finished: bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
lazy_static!(
 | 
			
		||||
    static ref PROCESSES: Mutex<RwLock<HashMap<usize, Proc>>> = Mutex::new(RwLock::new(HashMap::new()));
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
pub struct Downloader {
 | 
			
		||||
    count: usize,
 | 
			
		||||
    id_itr: usize,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Downloader {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            count: 0,
 | 
			
		||||
            id_itr: 0,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn download_all(&mut self, manifest: &Manifest, cfg: &ConfigWrapper) -> anyhow::Result<usize> {
 | 
			
		||||
        let format = manifest.format()?;
 | 
			
		||||
 | 
			
		||||
        for (genre, songs) in &manifest.genres {
 | 
			
		||||
            for song in songs {
 | 
			
		||||
                self.download_song(cfg, &song, &genre, &format).await?;
 | 
			
		||||
                self.wait_for_procs(10).await?;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        self.wait_for_procs(0).await?;
 | 
			
		||||
        Ok(self.count)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    pub async fn download_song(&mut self, cfg: &ConfigWrapper, song: &ManifestSong, genre: &String, format: &String) -> anyhow::Result<()> {
 | 
			
		||||
        let path = format!("{}/{genre}/{}.{}", cfg.cli.output, song.name, &format);
 | 
			
		||||
 | 
			
		||||
        if PathBuf::from(&path).exists() {
 | 
			
		||||
            log::debug!("File {path} exists, skipping");
 | 
			
		||||
            return Ok(())
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        log::debug!("File {path} doesnt exist, downloading");
 | 
			
		||||
        let mut cmd = if song.url.contains("youtube.com") || song.url.contains("youtu.be") {
 | 
			
		||||
            log::debug!("Song {} is from yotube", song.url);
 | 
			
		||||
            let mut cmd = tokio::process::Command::new(&cfg.cfg.ytdlp.path);
 | 
			
		||||
            cmd.args([
 | 
			
		||||
                    "-x",
 | 
			
		||||
                    "--audio-format",
 | 
			
		||||
                    format.as_str(),
 | 
			
		||||
                    "-o",
 | 
			
		||||
                    path.as_str(),
 | 
			
		||||
                    song.url.as_str()
 | 
			
		||||
                ]);
 | 
			
		||||
            cmd
 | 
			
		||||
        } else {
 | 
			
		||||
            let mut cmd = tokio::process::Command::new(&cfg.cfg.spotdl.path);
 | 
			
		||||
            cmd.args([
 | 
			
		||||
                    "-x",
 | 
			
		||||
                    "--audio-format",
 | 
			
		||||
                    format.as_str(),
 | 
			
		||||
                    "-o",
 | 
			
		||||
                    path.as_str(),
 | 
			
		||||
                    song.url.as_str()
 | 
			
		||||
                ]);
 | 
			
		||||
            cmd
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if log::max_level() < Level::Debug {
 | 
			
		||||
            cmd.stdout(Stdio::null()).stderr(Stdio::null());
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let mut proc = cmd.spawn()?;
 | 
			
		||||
        let id = self.id_itr;
 | 
			
		||||
        
 | 
			
		||||
        tokio::spawn(async move {
 | 
			
		||||
            let id = id;
 | 
			
		||||
            proc.wait().await
 | 
			
		||||
                .expect("child process encountered an error");
 | 
			
		||||
            PROCESSES.lock().await.write().await.get_mut(&id).unwrap().finished = true;
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        log::info!("Downloading {path}");
 | 
			
		||||
        PROCESSES.lock().await.write().await.insert(id, Proc {
 | 
			
		||||
            url: song.url.clone(),
 | 
			
		||||
            path,
 | 
			
		||||
            finished: false,
 | 
			
		||||
        });
 | 
			
		||||
        self.id_itr += 1;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn wait_for_procs(&mut self, until: usize) -> anyhow::Result<()> {
 | 
			
		||||
        // NOTE: This looks really fucked because i dont want to deadlock the processes so i lock PROCESSES for as little as possible
 | 
			
		||||
        // NOTE: So its also kinda really slow
 | 
			
		||||
        loop {
 | 
			
		||||
            {
 | 
			
		||||
                if PROCESSES.lock().await.read().await.len() <= until {
 | 
			
		||||
                    return Ok(());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let procs = {
 | 
			
		||||
                PROCESSES.lock().await.read().await.clone()
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            for (idx, proc) in procs {
 | 
			
		||||
                if proc.finished {
 | 
			
		||||
                    {
 | 
			
		||||
                        PROCESSES.lock().await.write().await.remove(&idx);
 | 
			
		||||
                    }
 | 
			
		||||
                    log::info!("Finished downloading {}", proc.path);
 | 
			
		||||
                    self.count += 1;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        #[allow(unreachable_code)] //? rust_analizer not smart enough for this
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,14 +1,14 @@
 | 
			
		|||
use log::LevelFilter;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
pub fn init_logger(debug: bool) {
 | 
			
		||||
    let level = if debug {
 | 
			
		||||
        LevelFilter::Debug
 | 
			
		||||
    } else {
 | 
			
		||||
        LevelFilter::Info
 | 
			
		||||
    };
 | 
			
		||||
    env_logger::builder()
 | 
			
		||||
        .format_timestamp(None)
 | 
			
		||||
        .filter_level(level)
 | 
			
		||||
        .init();
 | 
			
		||||
use log::LevelFilter;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
pub fn init_logger(debug: bool) {
 | 
			
		||||
    let level = if debug {
 | 
			
		||||
        LevelFilter::Debug
 | 
			
		||||
    } else {
 | 
			
		||||
        LevelFilter::Info
 | 
			
		||||
    };
 | 
			
		||||
    env_logger::builder()
 | 
			
		||||
        .format_timestamp(None)
 | 
			
		||||
        .filter_level(level)
 | 
			
		||||
        .init();
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,30 +1,30 @@
 | 
			
		|||
use config::ConfigWrapper;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// TODO: Possibly use https://docs.rs/ytextract/latest/ytextract/ instead of ytdlp
 | 
			
		||||
mod manifest;
 | 
			
		||||
mod logger;
 | 
			
		||||
mod downloader;
 | 
			
		||||
mod util;
 | 
			
		||||
mod commands;
 | 
			
		||||
mod prompt;
 | 
			
		||||
mod config;
 | 
			
		||||
mod constants;
 | 
			
		||||
 | 
			
		||||
#[tokio::main]
 | 
			
		||||
async fn main() {
 | 
			
		||||
    let Ok(cfg) = ConfigWrapper::parse().await else {
 | 
			
		||||
        return;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let mut manifest = match manifest::Manifest::from_path(&cfg.cli.manifest.as_std_path()) {
 | 
			
		||||
        Ok(m) => m,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Failed to parse manifest file {}: {e}", cfg.cli.manifest);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    let _ = commands::command_run(&cfg, &mut manifest).await;
 | 
			
		||||
}
 | 
			
		||||
use config::ConfigWrapper;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// TODO: Possibly use https://docs.rs/ytextract/latest/ytextract/ instead of ytdlp
 | 
			
		||||
mod manifest;
 | 
			
		||||
mod logger;
 | 
			
		||||
mod downloader;
 | 
			
		||||
mod util;
 | 
			
		||||
mod commands;
 | 
			
		||||
mod prompt;
 | 
			
		||||
mod config;
 | 
			
		||||
mod constants;
 | 
			
		||||
 | 
			
		||||
#[tokio::main]
 | 
			
		||||
async fn main() {
 | 
			
		||||
    let Ok(cfg) = ConfigWrapper::parse().await else {
 | 
			
		||||
        return;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let mut manifest = match manifest::Manifest::from_path(&cfg.cli.manifest.as_std_path()) {
 | 
			
		||||
        Ok(m) => m,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::error!("Failed to parse manifest file {}: {e}", cfg.cli.manifest);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    let _ = commands::command_run(&cfg, &mut manifest).await;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,76 +1,76 @@
 | 
			
		|||
use std::{collections::HashMap, fs::read_to_string, path::{Path, PathBuf}};
 | 
			
		||||
 | 
			
		||||
use anyhow::bail;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
const ALLOWED_FORMATS: &[&'static str] = &["m4a", "aac", "flac", "mp3", "vaw"];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type Genre = String;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct Manifest {
 | 
			
		||||
    #[serde(skip)]
 | 
			
		||||
    path: PathBuf,
 | 
			
		||||
    format: String,
 | 
			
		||||
    pub genres: HashMap<Genre, Vec<ManifestSong>>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Manifest {
 | 
			
		||||
    pub fn format(&self) -> anyhow::Result<String> {
 | 
			
		||||
        if !ALLOWED_FORMATS.contains(&self.format.as_str()) {
 | 
			
		||||
            log::error!("Unknown format, allowed formats: {}", ALLOWED_FORMATS.join(", "));
 | 
			
		||||
            bail!("")
 | 
			
		||||
        }
 | 
			
		||||
        Ok(self.format.clone())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct ManifestSong {
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub url: String
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
impl Manifest {
 | 
			
		||||
    fn from_string(s: String) -> anyhow::Result<Self> {
 | 
			
		||||
        let s = serde_json::from_str(&s)?;
 | 
			
		||||
        Ok(s)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn from_path(p: &Path) -> anyhow::Result<Self> {
 | 
			
		||||
        let data = read_to_string(p)?;
 | 
			
		||||
        let mut s = Self::from_string(data)?;
 | 
			
		||||
        s.path = p.to_path_buf();
 | 
			
		||||
        Ok(s)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn add_song(&mut self, genre: String, name: String, url: String) -> anyhow::Result<()> {
 | 
			
		||||
 | 
			
		||||
        if !self.genres.contains_key(&genre) {
 | 
			
		||||
            self.genres.insert(genre.clone(), Vec::new());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let Some(genre_ref) = self.genres.get_mut(&genre) else {
 | 
			
		||||
            log::error!("Invalid genre '{}'", genre);
 | 
			
		||||
            bail!("Invalid genre")
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        genre_ref.push(ManifestSong {
 | 
			
		||||
            name,
 | 
			
		||||
            url,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn save(&self) -> anyhow::Result<()> {
 | 
			
		||||
        let data = serde_json::to_string_pretty(self)?;
 | 
			
		||||
        std::fs::write(&self.path, data)?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
use std::{collections::HashMap, fs::read_to_string, path::{Path, PathBuf}};
 | 
			
		||||
 | 
			
		||||
use anyhow::bail;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
const ALLOWED_FORMATS: &[&'static str] = &["m4a", "aac", "flac", "mp3", "vaw"];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type Genre = String;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct Manifest {
 | 
			
		||||
    #[serde(skip)]
 | 
			
		||||
    path: PathBuf,
 | 
			
		||||
    format: String,
 | 
			
		||||
    pub genres: HashMap<Genre, Vec<ManifestSong>>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Manifest {
 | 
			
		||||
    pub fn format(&self) -> anyhow::Result<String> {
 | 
			
		||||
        if !ALLOWED_FORMATS.contains(&self.format.as_str()) {
 | 
			
		||||
            log::error!("Unknown format, allowed formats: {}", ALLOWED_FORMATS.join(", "));
 | 
			
		||||
            bail!("")
 | 
			
		||||
        }
 | 
			
		||||
        Ok(self.format.clone())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
pub struct ManifestSong {
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub url: String
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
impl Manifest {
 | 
			
		||||
    fn from_string(s: String) -> anyhow::Result<Self> {
 | 
			
		||||
        let s = serde_json::from_str(&s)?;
 | 
			
		||||
        Ok(s)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn from_path(p: &Path) -> anyhow::Result<Self> {
 | 
			
		||||
        let data = read_to_string(p)?;
 | 
			
		||||
        let mut s = Self::from_string(data)?;
 | 
			
		||||
        s.path = p.to_path_buf();
 | 
			
		||||
        Ok(s)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn add_song(&mut self, genre: String, name: String, url: String) -> anyhow::Result<()> {
 | 
			
		||||
 | 
			
		||||
        if !self.genres.contains_key(&genre) {
 | 
			
		||||
            self.genres.insert(genre.clone(), Vec::new());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let Some(genre_ref) = self.genres.get_mut(&genre) else {
 | 
			
		||||
            log::error!("Invalid genre '{}'", genre);
 | 
			
		||||
            bail!("Invalid genre")
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        genre_ref.push(ManifestSong {
 | 
			
		||||
            name,
 | 
			
		||||
            url,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn save(&self) -> anyhow::Result<()> {
 | 
			
		||||
        let data = serde_json::to_string_pretty(self)?;
 | 
			
		||||
        std::fs::write(&self.path, data)?;
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,148 +1,149 @@
 | 
			
		|||
use std::{collections::HashMap, io::Write};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
pub fn simple_prompt(p: &str) -> String {
 | 
			
		||||
 | 
			
		||||
    print!("{c}prompt{r}: {p} > ",
 | 
			
		||||
        c=anstyle::AnsiColor::Cyan.render_fg(),
 | 
			
		||||
        r=anstyle::Reset.render()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // I dont care if it fails
 | 
			
		||||
    let _ = std::io::stdout().flush();
 | 
			
		||||
    
 | 
			
		||||
    let mut buf = String::new();
 | 
			
		||||
    let _ = std::io::stdin().read_line(&mut buf);
 | 
			
		||||
 | 
			
		||||
    buf.trim().to_string()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn prompt_with_list(p: &str, options: &[&str]) -> usize {
 | 
			
		||||
    println!("{c}prompt{r}: {p}",
 | 
			
		||||
        c=anstyle::AnsiColor::Cyan.render_fg(),
 | 
			
		||||
        r=anstyle::Reset.render()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    for (i, op) in options.iter().enumerate() {
 | 
			
		||||
        println!("    - {}: {}", i, op);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    print!("> ");
 | 
			
		||||
    // I dont care if it fails
 | 
			
		||||
    let _ = std::io::stdout().flush();
 | 
			
		||||
    
 | 
			
		||||
    let mut buf = String::new();
 | 
			
		||||
    let _ = std::io::stdin().read_line(&mut buf);
 | 
			
		||||
 | 
			
		||||
    if let Ok(num) = buf.parse::<usize>() {
 | 
			
		||||
        if num <= options.len() {
 | 
			
		||||
            return num;
 | 
			
		||||
        } else {
 | 
			
		||||
            return prompt_with_list(p, options);
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        return prompt_with_list(p, options);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn prompt_with_list_or_str(p: &str, options: &[String]) -> String {
 | 
			
		||||
    println!("{c}prompt{r}: {p} (select with number or input text)",
 | 
			
		||||
        c=anstyle::AnsiColor::Cyan.render_fg(),
 | 
			
		||||
        r=anstyle::Reset.render()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    for (i, op) in options.iter().enumerate() {
 | 
			
		||||
        println!("    - {}: {}", i, op);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    print!("> ");
 | 
			
		||||
    // I dont care if it fails
 | 
			
		||||
    let _ = std::io::stdout().flush();
 | 
			
		||||
    
 | 
			
		||||
    let mut buf = String::new();
 | 
			
		||||
    let _ = std::io::stdin().read_line(&mut buf);
 | 
			
		||||
 | 
			
		||||
    if let Ok(num) = buf.trim().parse::<usize>() {
 | 
			
		||||
        if let Some(g) = options.get(num) {
 | 
			
		||||
            return g.clone();
 | 
			
		||||
        } else {
 | 
			
		||||
            return prompt_with_list_or_str(p, options);
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        return buf.trim().to_string();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
pub fn prompt_with_map(p: &str, options: HashMap<&str, &str>) -> String {
 | 
			
		||||
    println!("{c}prompt{r}: {p}",
 | 
			
		||||
        c=anstyle::AnsiColor::Cyan.render_fg(),
 | 
			
		||||
        r=anstyle::Reset.render()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let mut keys = Vec::new();
 | 
			
		||||
 | 
			
		||||
    for (k, v) in &options {
 | 
			
		||||
        println!("    - {}: {}", k, v);
 | 
			
		||||
        keys.push(k.trim().to_lowercase())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    print!("> ");
 | 
			
		||||
 | 
			
		||||
    // I dont care if it fails
 | 
			
		||||
    let _ = std::io::stdout().flush();
 | 
			
		||||
    
 | 
			
		||||
    let mut buf = String::new();
 | 
			
		||||
    let _ = std::io::stdin().read_line(&mut buf);
 | 
			
		||||
    if !keys.contains(&buf.trim().to_lowercase()) {
 | 
			
		||||
        return prompt_with_map(p, options);
 | 
			
		||||
    }
 | 
			
		||||
    buf.trim().to_string()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn prompt_bool(p: &str, default: Option<bool>) -> bool {
 | 
			
		||||
    if default == Some(true) {
 | 
			
		||||
        println!("{c}prompt{r}: {p} (Y/n)",
 | 
			
		||||
            c=anstyle::AnsiColor::Cyan.render_fg(),
 | 
			
		||||
            r=anstyle::Reset.render()
 | 
			
		||||
        );
 | 
			
		||||
    } else if default == Some(false) {
 | 
			
		||||
        println!("{c}prompt{r}: {p} (y/N)",
 | 
			
		||||
            c=anstyle::AnsiColor::Cyan.render_fg(),
 | 
			
		||||
            r=anstyle::Reset.render()
 | 
			
		||||
        );
 | 
			
		||||
    } else {
 | 
			
		||||
        println!("{c}prompt{r}: {p} (y/n)",
 | 
			
		||||
            c=anstyle::AnsiColor::Cyan.render_fg(),
 | 
			
		||||
            r=anstyle::Reset.render()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    print!("> ");
 | 
			
		||||
 | 
			
		||||
    // I dont care if it fails
 | 
			
		||||
    let _ = std::io::stdout().flush();
 | 
			
		||||
    
 | 
			
		||||
    let mut buf = String::new();
 | 
			
		||||
    let _ = std::io::stdin().read_line(&mut buf);
 | 
			
		||||
 | 
			
		||||
    if buf.trim().is_empty() {
 | 
			
		||||
        match default {
 | 
			
		||||
            Some(true) => return true,
 | 
			
		||||
            Some(false) => return false,
 | 
			
		||||
            None => {
 | 
			
		||||
                return prompt_bool(p, default);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    match buf.to_lowercase().trim() {
 | 
			
		||||
        "y" => true,
 | 
			
		||||
        "n" => false,
 | 
			
		||||
        c => {
 | 
			
		||||
            log::error!("'{c}' is invalid, type y (yes) or n (no)");
 | 
			
		||||
            return prompt_bool(p, default);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
use std::{collections::HashMap, io::Write};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
pub fn simple_prompt(p: &str) -> String {
 | 
			
		||||
 | 
			
		||||
    print!("{c}prompt{r}: {p} > ",
 | 
			
		||||
        c=anstyle::AnsiColor::Cyan.render_fg(),
 | 
			
		||||
        r=anstyle::Reset.render()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // I dont care if it fails
 | 
			
		||||
    let _ = std::io::stdout().flush();
 | 
			
		||||
    
 | 
			
		||||
    let mut buf = String::new();
 | 
			
		||||
    let _ = std::io::stdin().read_line(&mut buf);
 | 
			
		||||
 | 
			
		||||
    buf.trim().to_string()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub fn prompt_with_list(p: &str, options: &[&str]) -> usize {
 | 
			
		||||
    println!("{c}prompt{r}: {p}",
 | 
			
		||||
        c=anstyle::AnsiColor::Cyan.render_fg(),
 | 
			
		||||
        r=anstyle::Reset.render()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    for (i, op) in options.iter().enumerate() {
 | 
			
		||||
        println!("    - {}: {}", i, op);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    print!("> ");
 | 
			
		||||
    // I dont care if it fails
 | 
			
		||||
    let _ = std::io::stdout().flush();
 | 
			
		||||
    
 | 
			
		||||
    let mut buf = String::new();
 | 
			
		||||
    let _ = std::io::stdin().read_line(&mut buf);
 | 
			
		||||
 | 
			
		||||
    if let Ok(num) = buf.parse::<usize>() {
 | 
			
		||||
        if num <= options.len() {
 | 
			
		||||
            return num;
 | 
			
		||||
        } else {
 | 
			
		||||
            return prompt_with_list(p, options);
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        return prompt_with_list(p, options);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn prompt_with_list_or_str(p: &str, options: &[String]) -> String {
 | 
			
		||||
    println!("{c}prompt{r}: {p} (select with number or input text)",
 | 
			
		||||
        c=anstyle::AnsiColor::Cyan.render_fg(),
 | 
			
		||||
        r=anstyle::Reset.render()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    for (i, op) in options.iter().enumerate() {
 | 
			
		||||
        println!("    - {}: {}", i, op);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    print!("> ");
 | 
			
		||||
    // I dont care if it fails
 | 
			
		||||
    let _ = std::io::stdout().flush();
 | 
			
		||||
    
 | 
			
		||||
    let mut buf = String::new();
 | 
			
		||||
    let _ = std::io::stdin().read_line(&mut buf);
 | 
			
		||||
 | 
			
		||||
    if let Ok(num) = buf.trim().parse::<usize>() {
 | 
			
		||||
        if let Some(g) = options.get(num) {
 | 
			
		||||
            return g.clone();
 | 
			
		||||
        } else {
 | 
			
		||||
            return prompt_with_list_or_str(p, options);
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        return buf.trim().to_string();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#[allow(dead_code)]
 | 
			
		||||
pub fn prompt_with_map(p: &str, options: HashMap<&str, &str>) -> String {
 | 
			
		||||
    println!("{c}prompt{r}: {p}",
 | 
			
		||||
        c=anstyle::AnsiColor::Cyan.render_fg(),
 | 
			
		||||
        r=anstyle::Reset.render()
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let mut keys = Vec::new();
 | 
			
		||||
 | 
			
		||||
    for (k, v) in &options {
 | 
			
		||||
        println!("    - {}: {}", k, v);
 | 
			
		||||
        keys.push(k.trim().to_lowercase())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    print!("> ");
 | 
			
		||||
 | 
			
		||||
    // I dont care if it fails
 | 
			
		||||
    let _ = std::io::stdout().flush();
 | 
			
		||||
    
 | 
			
		||||
    let mut buf = String::new();
 | 
			
		||||
    let _ = std::io::stdin().read_line(&mut buf);
 | 
			
		||||
    if !keys.contains(&buf.trim().to_lowercase()) {
 | 
			
		||||
        return prompt_with_map(p, options);
 | 
			
		||||
    }
 | 
			
		||||
    buf.trim().to_string()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn prompt_bool(p: &str, default: Option<bool>) -> bool {
 | 
			
		||||
    if default == Some(true) {
 | 
			
		||||
        println!("{c}prompt{r}: {p} (Y/n)",
 | 
			
		||||
            c=anstyle::AnsiColor::Cyan.render_fg(),
 | 
			
		||||
            r=anstyle::Reset.render()
 | 
			
		||||
        );
 | 
			
		||||
    } else if default == Some(false) {
 | 
			
		||||
        println!("{c}prompt{r}: {p} (y/N)",
 | 
			
		||||
            c=anstyle::AnsiColor::Cyan.render_fg(),
 | 
			
		||||
            r=anstyle::Reset.render()
 | 
			
		||||
        );
 | 
			
		||||
    } else {
 | 
			
		||||
        println!("{c}prompt{r}: {p} (y/n)",
 | 
			
		||||
            c=anstyle::AnsiColor::Cyan.render_fg(),
 | 
			
		||||
            r=anstyle::Reset.render()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    print!("> ");
 | 
			
		||||
 | 
			
		||||
    // I dont care if it fails
 | 
			
		||||
    let _ = std::io::stdout().flush();
 | 
			
		||||
    
 | 
			
		||||
    let mut buf = String::new();
 | 
			
		||||
    let _ = std::io::stdin().read_line(&mut buf);
 | 
			
		||||
 | 
			
		||||
    if buf.trim().is_empty() {
 | 
			
		||||
        match default {
 | 
			
		||||
            Some(true) => return true,
 | 
			
		||||
            Some(false) => return false,
 | 
			
		||||
            None => {
 | 
			
		||||
                return prompt_bool(p, default);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    match buf.to_lowercase().trim() {
 | 
			
		||||
        "y" => true,
 | 
			
		||||
        "n" => false,
 | 
			
		||||
        c => {
 | 
			
		||||
            log::error!("'{c}' is invalid, type y (yes) or n (no)");
 | 
			
		||||
            return prompt_bool(p, default);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,54 +1,54 @@
 | 
			
		|||
use std::{io::Write, path::PathBuf};
 | 
			
		||||
 | 
			
		||||
use crate::constants;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
pub fn is_program_in_path(program: &str) -> Option<PathBuf> {
 | 
			
		||||
    if let Ok(path) = std::env::var("PATH") {
 | 
			
		||||
        for p in path.split(constants::PATH_VAR_SEP) {
 | 
			
		||||
            let exec_path = PathBuf::from(p).join(program).with_extension(constants::EXEC_EXT);
 | 
			
		||||
            if std::fs::metadata(&exec_path).is_ok() {
 | 
			
		||||
                return Some(exec_path);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    None
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(target_family="unix")]
 | 
			
		||||
pub fn isatty() -> bool {
 | 
			
		||||
    use std::{ffi::c_int, os::fd::AsRawFd};
 | 
			
		||||
    unsafe {
 | 
			
		||||
        let fd = std::io::stdin().as_raw_fd();
 | 
			
		||||
        libc::isatty(fd as c_int) == 1
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(target_family="windows")]
 | 
			
		||||
pub fn isatty() -> bool {
 | 
			
		||||
    unsafe {
 | 
			
		||||
        use windows::Win32::System::Console;
 | 
			
		||||
        use Console::{CONSOLE_MODE, STD_OUTPUT_HANDLE};
 | 
			
		||||
        let Ok(handle) = Console::GetStdHandle(STD_OUTPUT_HANDLE) else {
 | 
			
		||||
            return false;
 | 
			
		||||
        }; 
 | 
			
		||||
        
 | 
			
		||||
        let mut out = CONSOLE_MODE(0);
 | 
			
		||||
 | 
			
		||||
        let ret = Console::GetConsoleMode(handle, &mut out);
 | 
			
		||||
 | 
			
		||||
        ret.is_ok()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn dl_to_file(url: &str, p: PathBuf) -> anyhow::Result<()> {
 | 
			
		||||
    log::info!("Downloading {} -> {:?}", url, p);
 | 
			
		||||
    let ytdlp_req = reqwest::get(url).await?.bytes().await?;
 | 
			
		||||
    log::debug!("Downloading {:?} finished, writing to file", p);
 | 
			
		||||
    let mut fd = std::fs::File::create(&p)?;
 | 
			
		||||
    fd.write(&ytdlp_req)?;
 | 
			
		||||
    log::debug!("Finished writing {:?}", p);
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
use crate::constants;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
pub fn is_program_in_path(program: &str) -> Option<PathBuf> {
 | 
			
		||||
    if let Ok(path) = std::env::var("PATH") {
 | 
			
		||||
        for p in path.split(constants::PATH_VAR_SEP) {
 | 
			
		||||
            let exec_path = PathBuf::from(p).join(program).with_extension(constants::EXEC_EXT);
 | 
			
		||||
            if std::fs::metadata(&exec_path).is_ok() {
 | 
			
		||||
                return Some(exec_path);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    None
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(target_family="unix")]
 | 
			
		||||
pub fn isatty() -> bool {
 | 
			
		||||
    use std::{ffi::c_int, os::fd::AsRawFd};
 | 
			
		||||
    unsafe {
 | 
			
		||||
        let fd = std::io::stdin().as_raw_fd();
 | 
			
		||||
        libc::isatty(fd as c_int) == 1
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(target_family="windows")]
 | 
			
		||||
pub fn isatty() -> bool {
 | 
			
		||||
    unsafe {
 | 
			
		||||
        use windows::Win32::System::Console;
 | 
			
		||||
        use Console::{CONSOLE_MODE, STD_OUTPUT_HANDLE};
 | 
			
		||||
        let Ok(handle) = Console::GetStdHandle(STD_OUTPUT_HANDLE) else {
 | 
			
		||||
            return false;
 | 
			
		||||
        }; 
 | 
			
		||||
        
 | 
			
		||||
        let mut out = CONSOLE_MODE(0);
 | 
			
		||||
 | 
			
		||||
        let ret = Console::GetConsoleMode(handle, &mut out);
 | 
			
		||||
 | 
			
		||||
        ret.is_ok()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// pub async fn dl_to_file(url: &str, p: PathBuf) -> anyhow::Result<()> {
 | 
			
		||||
//     log::info!("Downloading {} -> {:?}", url, p);
 | 
			
		||||
//     let ytdlp_req = reqwest::get(url).await?.bytes().await?;
 | 
			
		||||
//     log::debug!("Downloading {:?} finished, writing to file", p);
 | 
			
		||||
//     let mut fd = std::fs::File::create(&p)?;
 | 
			
		||||
//     fd.write(&ytdlp_req)?;
 | 
			
		||||
//     log::debug!("Finished writing {:?}", p);
 | 
			
		||||
//     Ok(())
 | 
			
		||||
// }
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user