package arn import ( "errors" "os" "os/exec" "path" "sort" "strings" "github.com/aerogo/nano" "github.com/akyoto/color" "github.com/animenotifier/notify.moe/arn/autocorrect" ) // SoundTrack is a soundtrack used in one or multiple anime. type SoundTrack struct { Title SoundTrackTitle `json:"title" editable:"true"` Media []*ExternalMedia `json:"media" editable:"true"` Links []*Link `json:"links" editable:"true"` Lyrics SoundTrackLyrics `json:"lyrics" editable:"true"` Tags []string `json:"tags" editable:"true" tooltip:""` File string `json:"file"` hasID hasPosts hasCreator hasEditor hasLikes hasDraft } // Link returns the permalink for the track. func (track *SoundTrack) Link() string { return "/soundtrack/" + track.ID } // TitleByUser returns the preferred title for the given user. func (track *SoundTrack) TitleByUser(user *User) string { return track.Title.ByUser(user) } // MediaByService returns a slice of all media by the given service. func (track *SoundTrack) MediaByService(service string) []*ExternalMedia { filtered := []*ExternalMedia{} for _, media := range track.Media { if media.Service == service { filtered = append(filtered, media) } } return filtered } // HasMediaByService returns true if the track has media by the given service. func (track *SoundTrack) HasMediaByService(service string) bool { for _, media := range track.Media { if media.Service == service { return true } } return false } // HasTag returns true if it contains the given tag. func (track *SoundTrack) HasTag(search string) bool { for _, tag := range track.Tags { if tag == search { return true } } return false } // HasLyrics returns true if the track has lyrics in any language. func (track *SoundTrack) HasLyrics() bool { return track.Lyrics.Native != "" || track.Lyrics.Romaji != "" } // Anime fetches all tagged anime of the sound track. func (track *SoundTrack) Anime() []*Anime { var animeList []*Anime for _, tag := range track.Tags { if strings.HasPrefix(tag, "anime:") { animeID := strings.TrimPrefix(tag, "anime:") anime, err := GetAnime(animeID) if err != nil { if !track.IsDraft { color.Red("Error fetching anime: %v", err) } continue } animeList = append(animeList, anime) } } return animeList } // MainAnime ... func (track *SoundTrack) MainAnime() *Anime { allAnime := track.Anime() if len(allAnime) == 0 { return nil } return allAnime[0] } // TypeName returns the type name. func (track *SoundTrack) TypeName() string { return "SoundTrack" } // Self returns the object itself. func (track *SoundTrack) Self() Loggable { return track } // EditedByUser returns the user who edited this track last. func (track *SoundTrack) EditedByUser() *User { user, _ := GetUser(track.EditedBy) return user } // OnLike is called when the soundtrack receives a like. func (track *SoundTrack) OnLike(likedBy *User) { if likedBy.ID == track.CreatedBy { return } if !track.Creator().Settings().Notification.SoundTrackLikes { return } go func() { track.Creator().SendNotification(&PushNotification{ Title: likedBy.Nick + " liked your soundtrack " + track.Title.ByUser(track.Creator()), Message: likedBy.Nick + " liked your soundtrack " + track.Title.ByUser(track.Creator()) + ".", Icon: "https:" + likedBy.AvatarLink("large"), Link: "https://notify.moe" + likedBy.Link(), Type: NotificationTypeLike, }) }() } // Publish ... func (track *SoundTrack) Publish() error { // No media added if len(track.Media) == 0 { return errors.New("No media specified (at least 1 media source is required)") } animeFound := false for _, tag := range track.Tags { tag = autocorrect.Tag(tag) if strings.HasPrefix(tag, "anime:") { animeID := strings.TrimPrefix(tag, "anime:") _, err := GetAnime(animeID) if err != nil { return errors.New("Invalid anime ID") } animeFound = true } } // No anime found if !animeFound { return errors.New("Need to specify at least one anime") } // No tags if len(track.Tags) < 1 { return errors.New("Need to specify at least one tag") } // Publish err := publish(track) if err != nil { return err } // Start download in the background go func() { err := track.Download() if err == nil { track.Save() } }() return nil } // Unpublish ... func (track *SoundTrack) Unpublish() error { draftIndex, err := GetDraftIndex(track.CreatedBy) if err != nil { return err } if draftIndex.SoundTrackID != "" { return errors.New("You still have an unfinished draft") } track.IsDraft = true draftIndex.SoundTrackID = track.ID draftIndex.Save() return nil } // Download downloads the track. func (track *SoundTrack) Download() error { if track.IsDraft { return errors.New("Track is a draft") } youtubeVideos := track.MediaByService("Youtube") if len(youtubeVideos) == 0 { return errors.New("No Youtube ID") } youtubeID := youtubeVideos[0].ServiceID // Check for existing file if track.File != "" { stat, err := os.Stat(path.Join(Root, "audio", track.File)) if err == nil && !stat.IsDir() && stat.Size() > 0 { return errors.New("Already downloaded") } } audioDirectory := path.Join(Root, "audio") baseName := track.ID + "|" + youtubeID // Check if it exists on the file system fullPath := FindFileWithExtension(baseName, audioDirectory, []string{ ".opus", ".webm", ".ogg", ".m4a", ".mp3", ".flac", ".wav", }) // In case we added the file but didn't register it in database if fullPath != "" { extension := path.Ext(fullPath) track.File = baseName + extension return nil } filePath := path.Join(audioDirectory, baseName) // Use full URL to avoid problems with Youtube IDs that start with a hyphen url := "https://youtube.com/watch?v=" + youtubeID // Download cmd := exec.Command( "yt-dlp", "--no-check-certificate", "--extract-audio", "--audio-quality", "0", "--output", filePath+".%(ext)s", url, ) err := cmd.Start() if err != nil { return err } err = cmd.Wait() if err != nil { return err } // Find downloaded file fullPath = FindFileWithExtension(baseName, audioDirectory, []string{ ".opus", ".webm", ".ogg", ".m4a", ".mp3", ".flac", ".wav", }) extension := path.Ext(fullPath) track.File = baseName + extension return nil } // String implements the default string serialization. func (track *SoundTrack) String() string { return track.Title.ByUser(nil) } // SortSoundTracksLatestFirst ... func SortSoundTracksLatestFirst(tracks []*SoundTrack) { sort.Slice(tracks, func(i, j int) bool { return tracks[i].Created > tracks[j].Created }) } // SortSoundTracksPopularFirst ... func SortSoundTracksPopularFirst(tracks []*SoundTrack) { sort.Slice(tracks, func(i, j int) bool { aLikes := len(tracks[i].Likes) bLikes := len(tracks[j].Likes) if aLikes == bLikes { return tracks[i].Created > tracks[j].Created } return aLikes > bLikes }) } // GetSoundTrack ... func GetSoundTrack(id ID) (*SoundTrack, error) { track, err := DB.Get("SoundTrack", id) if err != nil { return nil, err } return track.(*SoundTrack), nil } // StreamSoundTracks returns a stream of all soundtracks. func StreamSoundTracks() <-chan *SoundTrack { channel := make(chan *SoundTrack, nano.ChannelBufferSize) go func() { for obj := range DB.All("SoundTrack") { channel <- obj.(*SoundTrack) } close(channel) }() return channel } // AllSoundTracks ... func AllSoundTracks() []*SoundTrack { all := make([]*SoundTrack, 0, DB.Collection("SoundTrack").Count()) for obj := range StreamSoundTracks() { all = append(all, obj) } return all } // FilterSoundTracks filters all soundtracks by a custom function. func FilterSoundTracks(filter func(*SoundTrack) bool) []*SoundTrack { var filtered []*SoundTrack for obj := range StreamSoundTracks() { if filter(obj) { filtered = append(filtered, obj) } } return filtered }