@@ -551,20 +551,190 @@ func HandleBatchDownload(episodes []models.Episode, animeURL string) error {
551551 return askAndPlayDownloadedEpisode (episodes , animeURL , startNum , endNum )
552552}
553553
554+ // HandleBatchDownloadRange performs batch download of episodes using a provided range.
555+ // It mirrors HandleBatchDownload but skips prompting for the range and enables optional
556+ // AniSkip sidecar generation when AllAnime Smart is enabled.
557+ func HandleBatchDownloadRange (episodes []models.Episode , animeURL string , startNum , endNum int ) error {
558+ start := time .Now ()
559+ if util .IsDebug {
560+ util .Logger .Debug ("HandleBatchDownloadRange started" , "animeURL" , animeURL , "start" , startNum , "end" , endNum )
561+ }
562+
563+ if startNum < 1 || endNum < startNum {
564+ return fmt .Errorf ("invalid episode range: %d-%d" , startNum , endNum )
565+ }
566+
567+ var (
568+ m * model
569+ p * tea.Program
570+ totalBytes int64
571+ httpClient = & http.Client {Transport : api .SafeTransport (10 * time .Second )}
572+ episodesToDownload []int
573+ )
574+
575+ // First pass: check which episodes need downloading and calculate total bytes
576+ for episodeNum := startNum ; episodeNum <= endNum ; episodeNum ++ {
577+ episode , found := findEpisode (episodes , episodeNum )
578+ if ! found {
579+ util .Logger .Warn ("Episode not found" , "episode" , episodeNum )
580+ continue
581+ }
582+
583+ episodePath , err := createEpisodePath (animeURL , episodeNum )
584+ if err != nil {
585+ util .Logger .Error ("Episode path error" , "episode" , episodeNum , "error" , err )
586+ continue
587+ }
588+ if fileExists (episodePath ) {
589+ util .Logger .Info ("Episode already exists" , "episode" , episodeNum )
590+ continue
591+ }
592+
593+ episodesToDownload = append (episodesToDownload , episodeNum )
594+
595+ videoURL , err := getBestQualityURL (episode , animeURL )
596+ if err != nil {
597+ util .Logger .Warn ("Skipping episode" , "episode" , episodeNum , "error" , err )
598+ continue
599+ }
600+ if sz , err := getContentLength (videoURL , httpClient ); err == nil {
601+ totalBytes += sz
602+ }
603+ }
604+
605+ if len (episodesToDownload ) == 0 {
606+ return handleExistingEpisodes (episodes , animeURL , startNum , endNum )
607+ }
608+
609+ fmt .Printf ("Found %d episode(s) to download...\n " , len (episodesToDownload ))
610+
611+ if totalBytes > 0 {
612+ m = & model {
613+ progress : progress .New (progress .WithDefaultGradient ()),
614+ keys : keyMap {quit : key .NewBinding (key .WithKeys ("ctrl+c" ), key .WithHelp ("ctrl+c" , "quit" ))},
615+ totalBytes : totalBytes ,
616+ }
617+ p = tea .NewProgram (m )
618+ }
619+
620+ downloadErrChan := make (chan error )
621+ go func () {
622+ var wg sync.WaitGroup
623+ sem := make (chan struct {}, 4 )
624+ for _ , epNum := range episodesToDownload {
625+ sem <- struct {}{}
626+ wg .Add (1 )
627+ go func (epNum int ) {
628+ defer func () { <- sem ; wg .Done () }()
629+ episode , found := findEpisode (episodes , epNum )
630+ if ! found {
631+ util .Logger .Warn ("Episode not found in batch" , "episode" , epNum )
632+ return
633+ }
634+
635+ videoURL , err := getBestQualityURL (episode , animeURL )
636+ if err != nil {
637+ util .Logger .Warn ("Skipping episode in batch" , "episode" , epNum , "error" , err )
638+ return
639+ }
640+ episodePath , err := createEpisodePath (animeURL , epNum )
641+ if err != nil {
642+ util .Logger .Error ("Episode path error" , "episode" , epNum , "error" , err )
643+ return
644+ }
645+
646+ if fileExists (episodePath ) {
647+ if p != nil {
648+ p .Send (statusMsg (fmt .Sprintf ("Episode %d already exists, skipping..." , epNum )))
649+ }
650+ return
651+ }
652+
653+ if p != nil {
654+ p .Send (statusMsg (fmt .Sprintf ("Downloading episode %d..." , epNum )))
655+ }
656+
657+ var dlErr error
658+ if strings .Contains (videoURL , ".m3u8" ) || strings .Contains (videoURL , ".mpd" ) || strings .Contains (videoURL , "repackager.wixmp.com" ) || strings .Contains (videoURL , "blogger.com" ) {
659+ dlErr = downloadWithYtDlp (videoURL , episodePath , m )
660+ } else {
661+ dlErr = DownloadVideo (videoURL , episodePath , 4 , m )
662+ }
663+ if dlErr != nil {
664+ util .Logger .Error ("Failed episode download" , "episode" , epNum , "error" , dlErr )
665+ return
666+ }
667+
668+ // Optional: write AniSkip sidecar when AllAnime Smart is enabled
669+ if util .GlobalDownloadRequest != nil && util .GlobalDownloadRequest .AllAnimeSmart {
670+ // Basic heuristic for AllAnime
671+ if strings .Contains (strings .ToLower (animeURL ), "allanime" ) || (len (animeURL ) < 30 && ! strings .Contains (animeURL , "http" )) {
672+ _ = api .WriteAniSkipSidecar (episodePath , & episode )
673+ }
674+ }
675+ }(epNum )
676+ }
677+ wg .Wait ()
678+
679+ if m != nil {
680+ if p != nil {
681+ p .Send (statusMsg ("All downloads completed!" ))
682+ }
683+ time .Sleep (500 * time .Millisecond )
684+ m .mu .Lock ()
685+ m .done = true
686+ m .mu .Unlock ()
687+ }
688+ downloadErrChan <- nil
689+ }()
690+
691+ if p != nil {
692+ if _ , err := p .Run (); err != nil {
693+ return fmt .Errorf ("progress UI error: %w" , err )
694+ }
695+ }
696+ if err := <- downloadErrChan ; err != nil {
697+ return err
698+ }
699+ fmt .Println ("\n All episodes downloaded successfully!" )
700+ if util .IsDebug {
701+ util .Logger .Debug ("HandleBatchDownloadRange completed" , "animeURL" , animeURL , "duration" , time .Since (start ))
702+ }
703+ return nil
704+ }
705+
554706// getEpisodeRange asks the user for the episode range for download.
555707func getEpisodeRange () (startNum , endNum int , err error ) {
556- prompt := promptui.Prompt {Label : "Enter start episode number" }
557- startStr , err := prompt .Run ()
558- if err != nil {
559- return 0 , 0 , err
560- }
561- prompt .Label = "Enter end episode number"
562- endStr , err := prompt .Run ()
563- if err != nil {
708+ var startStr , endStr string
709+
710+ form := huh .NewForm (
711+ huh .NewGroup (
712+ huh .NewInput ().
713+ Title ("Enter start episode number" ).
714+ Value (& startStr ).
715+ Validate (func (v string ) error {
716+ if _ , e := strconv .Atoi (strings .TrimSpace (v )); e != nil {
717+ return fmt .Errorf ("invalid number" )
718+ }
719+ return nil
720+ }),
721+ huh .NewInput ().
722+ Title ("Enter end episode number" ).
723+ Value (& endStr ).
724+ Validate (func (v string ) error {
725+ if _ , e := strconv .Atoi (strings .TrimSpace (v )); e != nil {
726+ return fmt .Errorf ("invalid number" )
727+ }
728+ return nil
729+ }),
730+ ),
731+ )
732+
733+ if err := form .Run (); err != nil {
564734 return 0 , 0 , err
565735 }
566- startNum , _ = strconv .Atoi (startStr )
567- endNum , _ = strconv .Atoi (endStr )
736+ startNum , _ = strconv .Atoi (strings . TrimSpace ( startStr ) )
737+ endNum , _ = strconv .Atoi (strings . TrimSpace ( endStr ) )
568738 if startNum > endNum {
569739 return 0 , 0 , fmt .Errorf ("start cannot be greater than end" )
570740 }
0 commit comments