@@ -19,29 +19,10 @@ import (
1919 "github.com/alvarorichard/Goanime/internal/api"
2020 "github.com/alvarorichard/Goanime/internal/models"
2121 "github.com/alvarorichard/Goanime/internal/util"
22+ "github.com/charmbracelet/huh"
2223 "github.com/ktr0731/go-fuzzyfinder"
2324)
2425
25- // WINDOWS RELEASE
26-
27- // func dialMPVSocket(socketPath string) (net.Conn, error) {
28- // if runtime.GOOS == "windows" {
29- // // //Attempt to connect using named pipe on Windows
30- // conn, err := winio.DialPipe(socketPath, nil)
31- // if err != nil {
32- // return nil, fmt.Errorf("failed to connect to named pipe: %w", err)
33- // }
34- // return conn, nil
35- // } else {
36- // // Unix-like system uses Unix sockets
37- // conn, err := net.Dial("unix", socketPath)
38- // if err != nil {
39- // return nil, fmt.Errorf("failed to connect to Unix socket: %w", err)
40- // }
41- // return conn, nil
42- // }
43- // }
44-
4526// DownloadFolderFormatter formats the anime URL to create a download folder name.
4627//
4728// This function extracts a specific part of the anime video URL to use it as the name
@@ -226,14 +207,44 @@ func SelectEpisodeWithFuzzyFinder(episodes []models.Episode) (string, string, er
226207
227208// ExtractEpisodeNumber extracts the episode number from the episode string
228209func ExtractEpisodeNumber (episodeStr string ) string {
229- // Handle formats like "Episode 100" or "100"
210+ // Try to extract numeric episode number from various patterns
211+ patterns := []string {
212+ `(?i)epis[oó]dio\s+(\d+)` , // "Episódio 1"
213+ `(?i)episode\s+(\d+)` , // "Episode 1"
214+ `(?i)ep\.?\s*(\d+)` , // "Ep 1" or "Ep. 1"
215+ `(?i)cap[íi]tulo\s+(\d+)` , // "Capítulo 1"
216+ `\b(\d+)\b` , // Any standalone number
217+ }
218+
219+ for _ , pattern := range patterns {
220+ re := regexp .MustCompile (pattern )
221+ matches := re .FindStringSubmatch (episodeStr )
222+ if len (matches ) >= 2 {
223+ return matches [1 ]
224+ }
225+ }
226+
227+ // Handle simple numeric cases by splitting
230228 parts := strings .Split (strings .TrimSpace (episodeStr ), " " )
231229 for _ , part := range parts {
232- if num , err := strconv .Atoi (part ); err == nil {
230+ // Clean common separators and try to parse
231+ cleanPart := strings .Trim (part , "()[]{}:.-_" )
232+ if num , err := strconv .Atoi (cleanPart ); err == nil && num > 0 {
233233 return fmt .Sprintf ("%d" , num )
234234 }
235235 }
236- return episodeStr // Fallback to original string if no number is found
236+
237+ // For movies, OVAs, and specials that don't have episode numbers, return "1"
238+ lowerStr := strings .ToLower (episodeStr )
239+ if strings .Contains (lowerStr , "filme" ) ||
240+ strings .Contains (lowerStr , "movie" ) ||
241+ strings .Contains (lowerStr , "ova" ) ||
242+ strings .Contains (lowerStr , "special" ) {
243+ return "1"
244+ }
245+
246+ // If no number found at all, return "1" as fallback (for single episodes/movies)
247+ return "1"
237248}
238249
239250// GetVideoURLForEpisode gets the video URL for a given episode URL
@@ -405,6 +416,7 @@ func findBloggerLink(content string) (string, error) {
405416 }
406417}
407418
419+ // extractActualVideoURL processes the video source and allows the user to select quality
408420func extractActualVideoURL (videoSrc string ) (string , error ) {
409421 if util .IsDebug {
410422 util .Debugf ("Processing video source: %s" , videoSrc )
@@ -414,7 +426,7 @@ func extractActualVideoURL(videoSrc string) (string, error) {
414426 return videoSrc , nil
415427 }
416428
417- // If the URL is from animefire.plus, we need to fetch it first
429+ // If the URL is from animefire.plus, fetch the content
418430 if strings .Contains (videoSrc , "animefire.plus/video/" ) {
419431 if util .IsDebug {
420432 util .Debugf ("Found animefire.plus video URL, fetching content..." )
@@ -437,7 +449,7 @@ func extractActualVideoURL(videoSrc string) (string, error) {
437449 return "" , fmt .Errorf ("failed to read video page: %w" , err )
438450 }
439451
440- // Try to parse as JSON first
452+ // Try to parse as JSON
441453 var videoResponse VideoResponse
442454 err = json .Unmarshal (body , & videoResponse )
443455 if err == nil && len (videoResponse .Data ) > 0 {
@@ -447,24 +459,60 @@ func extractActualVideoURL(videoSrc string) (string, error) {
447459 util .Debugf ("Available quality: %s -> %s" , v .Label , v .Src )
448460 }
449461 }
450- // If we have multiple qualities, pick the highest automatically
451- if len (videoResponse .Data ) > 1 {
452- best := videoResponse .Data [0 ]
453- bestQ , _ := strconv .Atoi (best .Label )
462+
463+ // If only one quality, use it directly
464+ if len (videoResponse .Data ) == 1 {
465+ return videoResponse .Data [0 ].Src , nil
466+ }
467+
468+ // Use global quality preference only if it's a specific quality (not "best")
469+ if util .GlobalQuality != "" && util .GlobalQuality != "best" {
470+ selectedSrc := selectQualityFromOptions (videoResponse .Data , util .GlobalQuality )
471+ if selectedSrc != "" {
472+ if util .IsDebug {
473+ util .Debugf ("Using global quality preference: %s -> %s" , util .GlobalQuality , selectedSrc )
474+ }
475+ return selectedSrc , nil
476+ }
477+ }
478+
479+ // Always prompt user for quality selection to maintain the 360p, 720p, 1080p options
480+ // Create options for huh.Select
481+ var options []huh.Option [string ]
482+ for _ , v := range videoResponse .Data {
483+ options = append (options , huh .NewOption (v .Label , v .Src ))
484+ }
485+
486+ // Present quality options to the user
487+ var selectedSrc string
488+ err := huh .NewSelect [string ]().
489+ Title ("Select Video Quality" ).
490+ Options (options ... ).
491+ Value (& selectedSrc ).
492+ Run ()
493+ if err != nil {
494+ return "" , fmt .Errorf ("failed to select quality: %w" , err )
495+ }
496+
497+ // Store the selected quality for future use in this session
498+ if util .GlobalQuality == "" || util .GlobalQuality == "best" {
499+ // Extract quality label from selected option
454500 for _ , v := range videoResponse .Data {
455- q , _ := strconv .Atoi (v .Label )
456- if q > bestQ {
457- best = v
458- bestQ = q
501+ if v .Src == selectedSrc {
502+ util .GlobalQuality = strings .ToLower (v .Label )
503+ if util .IsDebug {
504+ util .Debugf ("Storing selected quality for session: %s" , util .GlobalQuality )
505+ }
506+ break
459507 }
460508 }
461- return best .Src , nil
462509 }
463- // If only one quality, use it
464- return videoResponse .Data [0 ].Src , nil
510+
511+ // Return the selected source URL
512+ return selectedSrc , nil
465513 }
466514
467- // Try to find a direct video URL in the content
515+ // Fallback: Try to find a direct video URL in the content
468516 videoURLPattern := `https?://[^\s<>"]+?\.(?:mp4|m3u8)`
469517 re := regexp .MustCompile (videoURLPattern )
470518 matches := re .FindString (string (body ))
@@ -476,7 +524,7 @@ func extractActualVideoURL(videoSrc string) (string, error) {
476524 }
477525
478526 // Try to find a blogger link
479- videoSrc , err = findBloggerLink (string (body ))
527+ videoSrc , err : = findBloggerLink (string (body ))
480528 if err == nil && videoSrc != "" {
481529 if util .IsDebug {
482530 util .Debugf ("Found blogger link: %s" , videoSrc )
@@ -485,37 +533,88 @@ func extractActualVideoURL(videoSrc string) (string, error) {
485533 }
486534 }
487535
488- // Try to parse as JSON first
489- var videoResponse VideoResponse
490- err := json .Unmarshal ([]byte (videoSrc ), & videoResponse )
491- if err == nil && len (videoResponse .Data ) > 0 {
492- // If we have multiple qualities, pick the highest automatically
493- if len (videoResponse .Data ) > 1 {
494- best := videoResponse .Data [0 ]
495- bestQ , _ := strconv .Atoi (best .Label )
496- for _ , v := range videoResponse .Data {
497- q , _ := strconv .Atoi (v .Label )
498- if q > bestQ {
499- best = v
500- bestQ = q
501- }
536+ // If not JSON or no qualities found, return an error
537+ return "" , errors .New ("no valid video URL found" )
538+ }
539+
540+ // selectQualityFromOptions selects the best matching quality from available options
541+ func selectQualityFromOptions (videoData []VideoData , preferredQuality string ) string {
542+ if len (videoData ) == 0 {
543+ return ""
544+ }
545+
546+ // Normalize preferred quality string
547+ preferredQuality = strings .ToLower (strings .TrimSpace (preferredQuality ))
548+
549+ // Handle "best" quality preference
550+ if preferredQuality == "best" || preferredQuality == "" {
551+ // Find the highest quality (assume higher resolution numbers are better)
552+ bestQuality := videoData [0 ]
553+ for _ , v := range videoData {
554+ if extractResolution (v .Label ) > extractResolution (bestQuality .Label ) {
555+ bestQuality = v
502556 }
503- return best .Src , nil
504557 }
505- // If only one quality, use it
506- return videoResponse .Data [0 ].Src , nil
558+ return bestQuality .Src
507559 }
508560
509- // If not JSON, try to find a direct video URL
510- videoURLPattern := `https?://[^\s<>"]+?\.(?:mp4|m3u8)`
511- re := regexp .MustCompile (videoURLPattern )
512- matches := re .FindString (videoSrc )
561+ // Handle "worst" quality preference
562+ if preferredQuality == "worst" {
563+ // Find the lowest quality
564+ worstQuality := videoData [0 ]
565+ for _ , v := range videoData {
566+ if extractResolution (v .Label ) < extractResolution (worstQuality .Label ) {
567+ worstQuality = v
568+ }
569+ }
570+ return worstQuality .Src
571+ }
513572
514- if matches == "" {
515- return "" , errors .New ("no valid video URL found" )
573+ // Try to find exact match first
574+ for _ , v := range videoData {
575+ if strings .Contains (strings .ToLower (v .Label ), preferredQuality ) {
576+ return v .Src
577+ }
516578 }
517579
518- return matches , nil
580+ // If no exact match, try to find the closest resolution
581+ targetResolution := extractResolution (preferredQuality )
582+ if targetResolution > 0 {
583+ closestMatch := videoData [0 ]
584+ minDiff := abs (extractResolution (closestMatch .Label ) - targetResolution )
585+
586+ for _ , v := range videoData {
587+ diff := abs (extractResolution (v .Label ) - targetResolution )
588+ if diff < minDiff {
589+ minDiff = diff
590+ closestMatch = v
591+ }
592+ }
593+ return closestMatch .Src
594+ }
595+
596+ // Fallback to first option if nothing matches
597+ return videoData [0 ].Src
598+ }
599+
600+ // extractResolution extracts numeric resolution from quality label (e.g., "1080p" -> 1080)
601+ func extractResolution (label string ) int {
602+ re := regexp .MustCompile (`(\d+)p?` )
603+ matches := re .FindStringSubmatch (strings .ToLower (label ))
604+ if len (matches ) > 1 {
605+ if res , err := strconv .Atoi (matches [1 ]); err == nil {
606+ return res
607+ }
608+ }
609+ return 0
610+ }
611+
612+ // abs returns the absolute value of an integer
613+ func abs (x int ) int {
614+ if x < 0 {
615+ return - x
616+ }
617+ return x
519618}
520619
521620// VideoData represents the video data structure, with a source URL and a label
0 commit comments