package entertainment import ( "encoding/json" "fmt" "net/http" "net/url" "regexp" "strings" "time" "github.com/eternal-flame-AD/yoake/internal/auth" "github.com/eternal-flame-AD/yoake/internal/db" "github.com/eternal-flame-AD/yoake/internal/echoerror" "github.com/labstack/echo/v4" ) type YoutubeVideoStore struct { VideoID string `json:"video_id" form:"video_id" query:"video_id"` Meta *YoutubeVideoEmbedMeta `json:"meta,omitempty"` Tags []string `json:"tags"` Category string `param:"category" json:"category" form:"category" query:"category"` Comment string `json:"comment" form:"comment"` } type YoutubeVideoEmbedMeta struct { Title string `json:"title"` AuthorName string `json:"author_name"` AuthorURL string `json:"author_url"` Type string `json:"type"` ProviderName string `json:"provider_name"` ProviderURL string `json:"provider_url"` ThumbnailURL string `json:"thumbnail_url"` ThumbnailWidth int `json:"thumbnail_width"` ThumbnailHeight int `json:"thumbnail_height"` Html string `json:"html"` Version string `json:"version"` Height int `json:"height"` Width int `json:"width"` } func GetYoutubeVideoInfo(videoUrl string) (info *YoutubeVideoEmbedMeta, err error) { client := &http.Client{ Timeout: 10 * time.Second, } resp, err := client.Get("https://www.youtube.com/oembed?format=json&url=https://www.youtube.com/watch?v=" + url.QueryEscape(videoUrl)) if err != nil { return nil, err } defer resp.Body.Close() info = new(YoutubeVideoEmbedMeta) err = json.NewDecoder(resp.Body).Decode(info) if err != nil { return nil, err } return info, nil } type YoutubeCategoryStore struct { ID string `json:"id" form:"id"` DisplayName string `json:"display_name" form:"display_name"` } type YoutubeTagStore struct { ID string `json:"id" form:"id"` DisplayName string `json:"display_name" form:"display_name"` } type YoutubeVideoDBTxn struct { txn db.DBTxn } func newYoutubeDBTxn(txn db.DBTxn) *YoutubeVideoDBTxn { return &YoutubeVideoDBTxn{ txn: txn, } } func (t *YoutubeVideoDBTxn) GetCategories() (categories []YoutubeCategoryStore, err error) { err = db.GetJSON(t.txn, []byte("youtube_categories"), &categories) return } func (t *YoutubeVideoDBTxn) SetCategories(categories []YoutubeCategoryStore) (err error) { return db.SetJSON(t.txn, []byte("youtube_categories"), categories) } func (t *YoutubeVideoDBTxn) DeleteCategory(category string) (err error) { categories, err := t.GetCategories() if err != nil { return err } newCategories := make([]YoutubeCategoryStore, 0, len(categories)) for _, categoryS := range categories { if categoryS.ID != category { newCategories = append(newCategories, categoryS) } } if err = t.SetCategories(newCategories); err != nil { return err } if err := t.txn.Delete([]byte("youtube_category:" + category + "_tags")); err != nil && !db.IsNotFound(err) { return err } if err := t.txn.Delete([]byte("youtube_category:" + category + "_videos")); err != nil && !db.IsNotFound(err) { return err } return nil } func (t *YoutubeVideoDBTxn) GetTags(category string) (tags []YoutubeTagStore, err error) { categories, err := t.GetCategories() if err != nil { return nil, err } for _, categoryS := range categories { if categoryS.ID == category { err = db.GetJSON(t.txn, []byte("youtube_category:"+category+"_tags"), &tags) return } } return nil, echoerror.NewHttp(404, fmt.Errorf("category not found")) } func (t *YoutubeVideoDBTxn) SetTags(category string, tags []YoutubeTagStore) (err error) { categories, err := t.GetCategories() if err != nil { return err } for _, categoryS := range categories { if categoryS.ID == category { return db.SetJSON(t.txn, []byte("youtube_category:"+category+"_tags"), tags) } } return echoerror.NewHttp(404, fmt.Errorf("category not found")) } func (t *YoutubeVideoDBTxn) GetVideos(category string, tags []string) (videos []YoutubeVideoStore, err error) { videos = make([]YoutubeVideoStore, 0, 16) categories, err := t.GetCategories() if err != nil { return nil, err } for _, categoryS := range categories { if categoryS.ID == category { tagsAvail, err := t.GetTags(category) if err != nil { return nil, err } var tagSelected []string for _, tag := range tags { for _, tagAvail := range tagsAvail { if tagAvail.ID == tag { tagSelected = append(tagSelected, tag) break } } } var videosS []YoutubeVideoStore if err = db.GetJSON(t.txn, []byte("youtube_category:"+category+"_videos"), &videosS); err != nil { return nil, err } if len(tagSelected) == 0 { return videosS, nil } for _, video := range videosS { matchtag: for _, tagA := range tagSelected { for _, tag := range video.Tags { if tagA == tag { videos = append(videos, video) break matchtag } } } } } } return videos, nil } func (t *YoutubeVideoDBTxn) SetVideos(category string, videos []YoutubeVideoStore) (err error) { categories, err := t.GetCategories() if err != nil { return err } for _, categoryS := range categories { if categoryS.ID == category { existingTags, err := t.GetTags(category) if err != nil { return err } tagsUsed := make(map[string]YoutubeTagStore) for _, video := range videos { for _, tag := range video.Tags { found := false for _, existingTag := range existingTags { if existingTag.ID == tag { tagsUsed[tag] = existingTag found = true break } } if !found { return echoerror.NewHttp(400, fmt.Errorf("tag %s not found", tag)) } } } tagsUsedList := make([]YoutubeTagStore, 0, len(tagsUsed)) for _, tag := range tagsUsed { tagsUsedList = append(tagsUsedList, tag) } if err = t.SetTags(category, tagsUsedList); err != nil { return err } return db.SetJSON(t.txn, []byte("youtube_category:"+category+"_videos"), videos) } } return echoerror.NewHttp(404, fmt.Errorf("category not found")) } func registerYoutube(g *echo.Group, database db.DB) { g.GET("/categories", func(c echo.Context) error { txn := newYoutubeDBTxn(database.NewTransaction(false)) defer txn.txn.Discard() categories, err := txn.GetCategories() if err != nil && !db.IsNotFound(err) { return err } return c.JSON(http.StatusOK, categories) }) g.GET("/category/:category/tags", func(c echo.Context) error { txn := newYoutubeDBTxn(database.NewTransaction(false)) defer txn.txn.Discard() tags, err := txn.GetTags(c.Param("category")) if err != nil { return err } return c.JSON(http.StatusOK, tags) }) g.GET("/category/:category/videos", func(c echo.Context) error { tags := strings.Split(c.QueryParam("tags"), ",") txn := newYoutubeDBTxn(database.NewTransaction(false)) defer txn.txn.Discard() videos, err := txn.GetVideos(c.Param("category"), tags) if err != nil { return err } return c.JSON(http.StatusOK, videos) }) adminG := g.Group("", auth.RequireMiddleware(auth.RoleAdmin)) { adminG.POST("/categories", func(c echo.Context) error { var category YoutubeCategoryStore if err := c.Bind(&category); err != nil { return err } if category.DisplayName == "" { return echoerror.NewHttp(400, fmt.Errorf("display name is required")) } if category.ID == "" { category.ID = strings.ToLower( regexp.MustCompile(`[^0-9a-zA-Z]`).ReplaceAllString(category.DisplayName, "-")) } txn := newYoutubeDBTxn(database.NewTransaction(true)) defer txn.txn.Discard() updatedExisting := false existingCategories, err := txn.GetCategories() if err != nil { if !db.IsNotFound(err) { return err } } else { for i, existingCategory := range existingCategories { if existingCategory.ID == category.ID { existingCategories[i].DisplayName = category.DisplayName updatedExisting = true } } } if !updatedExisting { existingCategories = append(existingCategories, category) } if err = txn.SetCategories(existingCategories); err != nil { return err } if err := txn.txn.Commit(); err != nil { return err } return c.JSON(http.StatusOK, category) }) adminG.DELETE("/category/:category", func(c echo.Context) error { txn := newYoutubeDBTxn(database.NewTransaction(true)) defer txn.txn.Discard() if err := txn.DeleteCategory(c.Param("category")); err != nil { return err } if err := txn.txn.Commit(); err != nil { return err } return c.NoContent(http.StatusOK) }) adminG.POST("/category/:category/tags", func(c echo.Context) error { var tag YoutubeTagStore if err := c.Bind(&tag); err != nil { return err } if tag.DisplayName == "" { return echoerror.NewHttp(400, fmt.Errorf("display name is required")) } if tag.ID == "" { tag.ID = strings.ToLower( regexp.MustCompile(`[^0-9a-zA-Z]`).ReplaceAllString(tag.DisplayName, "-")) } txn := newYoutubeDBTxn(database.NewTransaction(true)) defer txn.txn.Discard() updatedExisting := false existingTags, err := txn.GetTags(c.Param("category")) if err != nil { if !db.IsNotFound(err) { return err } } else { for i, existingTag := range existingTags { if existingTag.ID == tag.ID { existingTags[i].DisplayName = tag.DisplayName updatedExisting = true } } } if !updatedExisting { existingTags = append(existingTags, tag) } if err = txn.SetTags(c.Param("category"), existingTags); err != nil { return err } if err := txn.txn.Commit(); err != nil { return err } return c.JSON(http.StatusOK, tag) }) adminG.POST("/category/:category/videos", func(c echo.Context) error { var video YoutubeVideoStore if err := c.Bind(&video); err != nil { return err } meta, err := GetYoutubeVideoInfo(video.VideoID) if err != nil { return err } video.Meta = meta txn := newYoutubeDBTxn(database.NewTransaction(true)) defer txn.txn.Discard() videos, err := txn.GetVideos(c.Param("category"), nil) updatedExisting := false if err != nil { if !db.IsNotFound(err) { return err } } else { for i, existingVideo := range videos { if existingVideo.VideoID == video.VideoID { videos[i].Tags = video.Tags videos[i].Category = video.Category videos[i].Meta = video.Meta videos[i].Comment = video.Comment updatedExisting = true } } } if !updatedExisting { videos = append(videos, video) } if err := txn.SetVideos(c.Param("category"), videos); err != nil { return err } if err := txn.txn.Commit(); err != nil { return err } return c.JSON(http.StatusOK, video) }) adminG.DELETE("/category/:category/video/:id", func(c echo.Context) error { txn := newYoutubeDBTxn(database.NewTransaction(true)) defer txn.txn.Discard() videos, err := txn.GetVideos(c.Param("category"), nil) if err != nil { return err } for i, video := range videos { if video.VideoID == c.Param("id") { videos = append(videos[:i], videos[i+1:]...) } } if err := txn.SetVideos(c.Param("category"), videos); err != nil { return err } if err := txn.txn.Commit(); err != nil { return err } return c.NoContent(http.StatusOK) }) } }