Skip to content

Commit

Permalink
Determine release time via YouTube API
Browse files Browse the repository at this point in the history
  • Loading branch information
pseudoscalar committed Nov 5, 2021
1 parent 2ba960a commit eb30fa4
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 95 deletions.
131 changes: 42 additions & 89 deletions local/local.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
package local

import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"regexp"
"strings"
"time"

log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"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/tags_manager"
)
Expand All @@ -24,6 +21,7 @@ type SyncContext struct {
LbrynetAddr string
ChannelID string
PublishBid float64
YouTubeSourceConfig *YouTubeSourceConfig
}

func (c *SyncContext) Validate() error {
Expand All @@ -42,6 +40,10 @@ func (c *SyncContext) Validate() error {
return nil
}

type YouTubeSourceConfig struct {
YouTubeAPIKey string
}

var syncContext SyncContext

func AddCommand(rootCmd *cobra.Command) {
Expand All @@ -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().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")

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

Expand All @@ -71,11 +77,9 @@ func localCmd(cmd *cobra.Command, args []string) {
log.Error(err)
return
}
fmt.Println(syncContext.LbrynetAddr)

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
publisher, err = NewLocalSDKPublisher(syncContext.LbrynetAddr, syncContext.ChannelID, syncContext.PublishBid)
Expand All @@ -85,10 +89,12 @@ func localCmd(cmd *cobra.Command, args []string) {
}

var videoSource VideoSource
videoSource, err = NewYtdlVideoSource(syncContext.TempDir)
if err != nil {
log.Errorf("Error setting up video source: %v", err)
return
if syncContext.YouTubeSourceConfig != nil {
videoSource, err = NewYtdlVideoSource(syncContext.TempDir, syncContext.YouTubeSourceConfig)
if err != nil {
log.Errorf("Error setting up video source: %v", err)
return
}
}

sourceVideo, err := videoSource.GetVideo(videoID)
Expand Down Expand Up @@ -116,78 +122,6 @@ func localCmd(cmd *cobra.Command, args []string) {
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 {
ID string
Title *string
Expand Down Expand Up @@ -243,9 +177,14 @@ func processVideoForPublishing(source SourceVideo, channelID string) (*Publishab

claimName := namer.NewNamer().GetNextName(title)

thumbnailURL := ""
if source.ThumbnailURL != nil {
thumbnailURL = *source.ThumbnailURL
thumbnailURL := source.ThumbnailURL
if thumbnailURL == nil {
thumbnailURL = util.PtrToString("")
}

releaseTime := source.ReleaseTime
if releaseTime == nil {
releaseTime = util.PtrToInt64(time.Now().Unix())
}

processed := PublishableVideo {
Expand All @@ -254,8 +193,8 @@ func processVideoForPublishing(source SourceVideo, channelID string) (*Publishab
Description: getAbbrevDescription(source),
Languages: languages,
Tags: tags,
ReleaseTime: *source.ReleaseTime,
ThumbnailURL: thumbnailURL,
ReleaseTime: *releaseTime,
ThumbnailURL: *thumbnailURL,
FullLocalPath: source.FullLocalPath,
}

Expand All @@ -264,6 +203,20 @@ func processVideoForPublishing(source SourceVideo, channelID string) (*Publishab
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 {
GetVideo(id string) (*SourceVideo, error)
}
Expand Down
45 changes: 45 additions & 0 deletions local/youtubeEnricher.go
Original file line number Diff line number Diff line change
@@ -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 changes: 83 additions & 0 deletions local/ytapi.go
Original file line number Diff line number Diff line change
@@ -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"`
}
Loading

0 comments on commit eb30fa4

Please sign in to comment.