Determine release time via YouTube API

This commit is contained in:
pseudoscalar 2021-11-04 15:28:28 -05:00
parent 2ba960ae01
commit eb30fa4299
4 changed files with 194 additions and 95 deletions

View file

@ -1,20 +1,17 @@
package local package local
import ( import (
"encoding/json"
"errors" "errors"
"fmt"
"io/ioutil"
"os" "os"
"path"
"regexp" "regexp"
"strings" "strings"
"time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/abadojack/whatlanggo" "github.com/abadojack/whatlanggo"
"github.com/lbryio/ytsync/v5/downloader/ytdl" "github.com/lbryio/lbry.go/v2/extras/util"
"github.com/lbryio/ytsync/v5/namer" "github.com/lbryio/ytsync/v5/namer"
"github.com/lbryio/ytsync/v5/tags_manager" "github.com/lbryio/ytsync/v5/tags_manager"
) )
@ -24,6 +21,7 @@ type SyncContext struct {
LbrynetAddr string LbrynetAddr string
ChannelID string ChannelID string
PublishBid float64 PublishBid float64
YouTubeSourceConfig *YouTubeSourceConfig
} }
func (c *SyncContext) Validate() error { func (c *SyncContext) Validate() error {
@ -42,6 +40,10 @@ func (c *SyncContext) Validate() error {
return nil return nil
} }
type YouTubeSourceConfig struct {
YouTubeAPIKey string
}
var syncContext SyncContext var syncContext SyncContext
func AddCommand(rootCmd *cobra.Command) { func AddCommand(rootCmd *cobra.Command) {
@ -55,6 +57,10 @@ func AddCommand(rootCmd *cobra.Command) {
cmd.Flags().Float64Var(&syncContext.PublishBid, "publish-bid", 0.01, "Bid amount for the stream claim") cmd.Flags().Float64Var(&syncContext.PublishBid, "publish-bid", 0.01, "Bid amount for the stream claim")
cmd.Flags().StringVar(&syncContext.LbrynetAddr, "lbrynet-address", getEnvDefault("LBRYNET_ADDRESS", ""), "JSONRPC address of the local LBRYNet daemon") cmd.Flags().StringVar(&syncContext.LbrynetAddr, "lbrynet-address", getEnvDefault("LBRYNET_ADDRESS", ""), "JSONRPC address of the local LBRYNet daemon")
cmd.Flags().StringVar(&syncContext.ChannelID, "channel-id", "", "LBRY channel ID to publish to") cmd.Flags().StringVar(&syncContext.ChannelID, "channel-id", "", "LBRY channel ID to publish to")
// For now, assume source is always YouTube
syncContext.YouTubeSourceConfig = &YouTubeSourceConfig{}
cmd.Flags().StringVar(&syncContext.YouTubeSourceConfig.YouTubeAPIKey, "youtube-api-key", getEnvDefault("YOUTUBE_API_KEY", ""), "YouTube API Key")
rootCmd.AddCommand(cmd) rootCmd.AddCommand(cmd)
} }
@ -71,11 +77,9 @@ func localCmd(cmd *cobra.Command, args []string) {
log.Error(err) log.Error(err)
return return
} }
fmt.Println(syncContext.LbrynetAddr)
videoID := args[0] videoID := args[0]
log.Debugf("Running sync for YouTube video ID %s", videoID) log.Debugf("Running sync for video ID %s", videoID)
var publisher VideoPublisher var publisher VideoPublisher
publisher, err = NewLocalSDKPublisher(syncContext.LbrynetAddr, syncContext.ChannelID, syncContext.PublishBid) publisher, err = NewLocalSDKPublisher(syncContext.LbrynetAddr, syncContext.ChannelID, syncContext.PublishBid)
@ -85,10 +89,12 @@ func localCmd(cmd *cobra.Command, args []string) {
} }
var videoSource VideoSource var videoSource VideoSource
videoSource, err = NewYtdlVideoSource(syncContext.TempDir) if syncContext.YouTubeSourceConfig != nil {
if err != nil { videoSource, err = NewYtdlVideoSource(syncContext.TempDir, syncContext.YouTubeSourceConfig)
log.Errorf("Error setting up video source: %v", err) if err != nil {
return log.Errorf("Error setting up video source: %v", err)
return
}
} }
sourceVideo, err := videoSource.GetVideo(videoID) sourceVideo, err := videoSource.GetVideo(videoID)
@ -116,78 +122,6 @@ func localCmd(cmd *cobra.Command, args []string) {
log.Info("Done") log.Info("Done")
} }
func getVideoMetadata(basePath, videoID string) (*ytdl.YtdlVideo, string, error) {
metadataPath := basePath + ".info.json"
_, err := os.Stat(metadataPath)
if err != nil && !os.IsNotExist(err) {
log.Errorf("Error determining if video metadata already exists: %v", err)
return nil, "", err
} else if err == nil {
log.Debugf("Video metadata file %s already exists. Attempting to load existing file.", metadataPath)
videoMetadata, err := loadVideoMetadata(metadataPath)
if err != nil {
log.Debugf("Error loading pre-existing video metadata: %v. Deleting file and attempting re-download.", err)
} else {
return videoMetadata, metadataPath, nil
}
}
if err := downloadVideoMetadata(basePath, videoID); err != nil {
return nil, "", err
}
videoMetadata, err := loadVideoMetadata(metadataPath)
return videoMetadata, metadataPath, err
}
func loadVideoMetadata(path string) (*ytdl.YtdlVideo, error) {
metadataBytes, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var videoMetadata *ytdl.YtdlVideo
err = json.Unmarshal(metadataBytes, &videoMetadata)
if err != nil {
return nil, err
}
return videoMetadata, nil
}
func getVideoDownloadedPath(videoDir, videoID string) (string, error) {
files, err := ioutil.ReadDir(videoDir)
if err != nil {
return "", err
}
for _, f := range files {
if f.IsDir() {
continue
}
if path.Ext(f.Name()) == ".mp4" && strings.Contains(f.Name(), videoID) {
return path.Join(videoDir, f.Name()), nil
}
}
return "", errors.New("could not find any downloaded videos")
}
func getAbbrevDescription(v SourceVideo) string {
if v.Description == nil {
return v.SourceURL
}
maxLength := 2800
description := strings.TrimSpace(*v.Description)
additionalDescription := "\n" + v.SourceURL
if len(description) > maxLength {
description = description[:maxLength]
}
return description + "\n..." + additionalDescription
}
type SourceVideo struct { type SourceVideo struct {
ID string ID string
Title *string Title *string
@ -243,9 +177,14 @@ func processVideoForPublishing(source SourceVideo, channelID string) (*Publishab
claimName := namer.NewNamer().GetNextName(title) claimName := namer.NewNamer().GetNextName(title)
thumbnailURL := "" thumbnailURL := source.ThumbnailURL
if source.ThumbnailURL != nil { if thumbnailURL == nil {
thumbnailURL = *source.ThumbnailURL thumbnailURL = util.PtrToString("")
}
releaseTime := source.ReleaseTime
if releaseTime == nil {
releaseTime = util.PtrToInt64(time.Now().Unix())
} }
processed := PublishableVideo { processed := PublishableVideo {
@ -254,8 +193,8 @@ func processVideoForPublishing(source SourceVideo, channelID string) (*Publishab
Description: getAbbrevDescription(source), Description: getAbbrevDescription(source),
Languages: languages, Languages: languages,
Tags: tags, Tags: tags,
ReleaseTime: *source.ReleaseTime, ReleaseTime: *releaseTime,
ThumbnailURL: thumbnailURL, ThumbnailURL: *thumbnailURL,
FullLocalPath: source.FullLocalPath, FullLocalPath: source.FullLocalPath,
} }
@ -264,6 +203,20 @@ func processVideoForPublishing(source SourceVideo, channelID string) (*Publishab
return &processed, nil return &processed, nil
} }
func getAbbrevDescription(v SourceVideo) string {
if v.Description == nil {
return v.SourceURL
}
maxLength := 2800
description := strings.TrimSpace(*v.Description)
additionalDescription := "\n" + v.SourceURL
if len(description) > maxLength {
description = description[:maxLength]
}
return description + "\n..." + additionalDescription
}
type VideoSource interface { type VideoSource interface {
GetVideo(id string) (*SourceVideo, error) GetVideo(id string) (*SourceVideo, error)
} }

45
local/youtubeEnricher.go Normal file
View file

@ -0,0 +1,45 @@
package local
import (
"time"
log "github.com/sirupsen/logrus"
"github.com/lbryio/lbry.go/v2/extras/util"
)
type YouTubeVideoEnricher interface {
EnrichMissing(source *SourceVideo) error
}
type YouTubeAPIVideoEnricher struct {
api *YouTubeAPI
}
func NewYouTubeAPIVideoEnricher(apiKey string) (*YouTubeAPIVideoEnricher) {
enricher := YouTubeAPIVideoEnricher{
api: NewYouTubeAPI(apiKey),
}
return &enricher
}
func (e *YouTubeAPIVideoEnricher) EnrichMissing(source *SourceVideo) error {
if source.ReleaseTime != nil {
log.Debugf("Video %s does not need enrichment. YouTubeAPIVideoEnricher is skipping.", source.ID)
return nil
}
snippet, err := e.api.GetVideoSnippet(source.ID)
if err != nil {
log.Errorf("Error snippet data for video %s: %v", err)
return err
}
publishedAt, err := time.Parse(time.RFC3339, snippet.PublishedAt)
if err != nil {
log.Errorf("Error converting publishedAt to timestamp: %v", err)
} else {
source.ReleaseTime = util.PtrToInt64(publishedAt.Unix())
}
return nil
}

83
local/ytapi.go Normal file
View file

@ -0,0 +1,83 @@
package local
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
log "github.com/sirupsen/logrus"
)
type YouTubeAPI struct {
apiKey string
client *http.Client
}
func NewYouTubeAPI(apiKey string) (*YouTubeAPI) {
client := &http.Client {
Transport: &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableCompression: true,
},
}
api := YouTubeAPI {
apiKey: apiKey,
client: client,
}
return &api
}
func (a *YouTubeAPI) GetVideoSnippet(videoID string) (*VideoSnippet, error) {
req, err := http.NewRequest("GET", "https://youtube.googleapis.com/youtube/v3/videos", nil)
if err != nil {
log.Errorf("Error creating http client for YouTube API: %v", err)
return nil, err
}
query := req.URL.Query()
query.Add("part", "snippet")
query.Add("id", videoID)
query.Add("key", a.apiKey)
req.URL.RawQuery = query.Encode()
req.Header.Add("Accept", "application/json")
resp, err := a.client.Do(req)
defer resp.Body.Close()
if err != nil {
log.Errorf("Error from YouTube API: %v", err)
return nil, err
}
body, err := io.ReadAll(resp.Body)
log.Tracef("Response from YouTube API: %s", string(body[:]))
var result videoListResponse
err = json.Unmarshal(body, &result)
if err != nil {
log.Errorf("Error deserializing video list response from YouTube API: %v", err)
return nil, err
}
if len(result.Items) != 1 {
err = fmt.Errorf("YouTube API responded with incorrect number of snippets (%d) while attempting to get snippet data for video %s", len(result.Items), videoID)
return nil, err
}
return &result.Items[0].Snippet, nil
}
type videoListResponse struct {
Items []struct {
Snippet VideoSnippet `json:"snippet"`
} `json:"items"`
}
type VideoSnippet struct {
PublishedAt string `json:"publishedAt"`
}

View file

@ -1,18 +1,17 @@
package local package local
import ( import (
"time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/lbryio/lbry.go/v2/extras/util" "github.com/lbryio/ytsync/v5/downloader/ytdl"
) )
type YtdlVideoSource struct { type YtdlVideoSource struct {
downloader Ytdl downloader Ytdl
enrichers []YouTubeVideoEnricher
} }
func NewYtdlVideoSource(downloadDir string) (*YtdlVideoSource, error) { func NewYtdlVideoSource(downloadDir string, config *YouTubeSourceConfig) (*YtdlVideoSource, error) {
ytdl, err := NewYtdl(downloadDir) ytdl, err := NewYtdl(downloadDir)
if err != nil { if err != nil {
return nil, err return nil, err
@ -22,6 +21,11 @@ func NewYtdlVideoSource(downloadDir string) (*YtdlVideoSource, error) {
downloader: *ytdl, downloader: *ytdl,
} }
if config.YouTubeAPIKey != "" {
ytapiEnricher := NewYouTubeAPIVideoEnricher(config.YouTubeAPIKey)
source.enrichers = append(source.enrichers, ytapiEnricher)
}
return &source, nil return &source, nil
} }
@ -36,6 +40,13 @@ func (s *YtdlVideoSource) GetVideo(id string) (*SourceVideo, error) {
return nil, err return nil, err
} }
var bestThumbnail *ytdl.Thumbnail = nil
for i, thumbnail := range metadata.Thumbnails {
if i == 0 || bestThumbnail.Width < thumbnail.Width {
bestThumbnail = &thumbnail
}
}
sourceVideo := SourceVideo { sourceVideo := SourceVideo {
ID: id, ID: id,
Title: &metadata.Title, Title: &metadata.Title,
@ -43,11 +54,18 @@ func (s *YtdlVideoSource) GetVideo(id string) (*SourceVideo, error) {
SourceURL: "\nhttps://www.youtube.com/watch?v=" + id, SourceURL: "\nhttps://www.youtube.com/watch?v=" + id,
Languages: []string{}, Languages: []string{},
Tags: metadata.Tags, Tags: metadata.Tags,
ReleaseTime: util.PtrToInt64(time.Now().Unix()), ReleaseTime: nil,
ThumbnailURL: nil, ThumbnailURL: &bestThumbnail.URL,
FullLocalPath: videoPath, FullLocalPath: videoPath,
} }
for _, enricher := range s.enrichers {
err = enricher.EnrichMissing(&sourceVideo)
if err != nil {
log.Warnf("Error enriching video %s, continuing enrichment: %v", id, err)
}
}
log.Debugf("Source video retrieved via ytdl: %v", sourceVideo) log.Debugf("Source video retrieved via ytdl: %v", sourceVideo)
return &sourceVideo, nil return &sourceVideo, nil