yoake/internal/entertainment/youtube.go
2022-11-11 17:16:53 -05:00

421 lines
11 KiB
Go

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 {
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)
})
}
}