2017-12-28 18:14:33 +01:00
|
|
|
package sources
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"regexp"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
2018-08-23 00:28:31 +02:00
|
|
|
"sync"
|
2018-09-18 21:20:34 +02:00
|
|
|
"time"
|
2018-08-23 00:28:31 +02:00
|
|
|
|
2018-03-09 17:47:38 +01:00
|
|
|
"github.com/lbryio/lbry.go/errors"
|
2017-12-28 18:14:33 +01:00
|
|
|
"github.com/lbryio/lbry.go/jsonrpc"
|
2018-10-08 22:19:17 +02:00
|
|
|
"github.com/lbryio/ytsync/namer"
|
2017-12-28 18:14:33 +01:00
|
|
|
|
2018-08-24 17:57:45 +02:00
|
|
|
"github.com/rylio/ytdl"
|
2017-12-28 18:14:33 +01:00
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"google.golang.org/api/youtube/v3"
|
|
|
|
)
|
|
|
|
|
|
|
|
type YoutubeVideo struct {
|
|
|
|
id string
|
|
|
|
channelTitle string
|
|
|
|
title string
|
|
|
|
description string
|
|
|
|
playlistPosition int64
|
2018-08-14 17:09:23 +02:00
|
|
|
size *int64
|
2018-09-18 23:28:25 +02:00
|
|
|
maxVideoSize int64
|
2017-12-28 18:14:33 +01:00
|
|
|
publishedAt time.Time
|
|
|
|
dir string
|
2018-08-23 00:28:31 +02:00
|
|
|
claimNames map[string]bool
|
|
|
|
syncedVideosMux *sync.RWMutex
|
2017-12-28 18:14:33 +01:00
|
|
|
}
|
|
|
|
|
2018-08-17 16:05:54 +02:00
|
|
|
func NewYoutubeVideo(directory string, snippet *youtube.PlaylistItemSnippet) *YoutubeVideo {
|
2017-12-28 18:14:33 +01:00
|
|
|
publishedAt, _ := time.Parse(time.RFC3339Nano, snippet.PublishedAt) // ignore parse errors
|
2018-08-17 16:05:54 +02:00
|
|
|
return &YoutubeVideo{
|
2017-12-28 18:14:33 +01:00
|
|
|
id: snippet.ResourceId.VideoId,
|
|
|
|
title: snippet.Title,
|
|
|
|
description: snippet.Description,
|
|
|
|
channelTitle: snippet.ChannelTitle,
|
|
|
|
playlistPosition: snippet.Position,
|
|
|
|
publishedAt: publishedAt,
|
|
|
|
dir: directory,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-17 16:05:54 +02:00
|
|
|
func (v *YoutubeVideo) ID() string {
|
2017-12-28 18:14:33 +01:00
|
|
|
return v.id
|
|
|
|
}
|
|
|
|
|
2018-08-17 16:05:54 +02:00
|
|
|
func (v *YoutubeVideo) PlaylistPosition() int {
|
2018-04-25 20:56:26 +02:00
|
|
|
return int(v.playlistPosition)
|
|
|
|
}
|
|
|
|
|
2018-08-17 16:05:54 +02:00
|
|
|
func (v *YoutubeVideo) IDAndNum() string {
|
2017-12-28 18:14:33 +01:00
|
|
|
return v.ID() + " (" + strconv.Itoa(int(v.playlistPosition)) + " in channel)"
|
|
|
|
}
|
|
|
|
|
2018-08-17 16:05:54 +02:00
|
|
|
func (v *YoutubeVideo) PublishedAt() time.Time {
|
2017-12-28 18:14:33 +01:00
|
|
|
return v.publishedAt
|
|
|
|
}
|
|
|
|
|
2018-10-09 21:57:07 +02:00
|
|
|
func (v *YoutubeVideo) getFullPath() string {
|
2018-05-05 13:22:33 +02:00
|
|
|
maxLen := 30
|
2017-12-28 18:14:33 +01:00
|
|
|
reg := regexp.MustCompile(`[^a-zA-Z0-9]+`)
|
|
|
|
|
|
|
|
chunks := strings.Split(strings.ToLower(strings.Trim(reg.ReplaceAllString(v.title, "-"), "-")), "-")
|
|
|
|
|
|
|
|
name := chunks[0]
|
|
|
|
if len(name) > maxLen {
|
2018-08-10 14:41:21 +02:00
|
|
|
name = name[:maxLen]
|
2017-12-28 18:14:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, chunk := range chunks[1:] {
|
|
|
|
tmpName := name + "-" + chunk
|
|
|
|
if len(tmpName) > maxLen {
|
|
|
|
if len(name) < 20 {
|
|
|
|
name = tmpName[:maxLen]
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
name = tmpName
|
|
|
|
}
|
2018-06-04 16:35:35 +02:00
|
|
|
if len(name) < 1 {
|
|
|
|
name = v.id
|
|
|
|
}
|
2018-07-24 02:01:35 +02:00
|
|
|
return v.videoDir() + "/" + name + ".mp4"
|
2017-12-28 18:14:33 +01:00
|
|
|
}
|
|
|
|
|
2018-08-17 16:05:54 +02:00
|
|
|
func (v *YoutubeVideo) getAbbrevDescription() string {
|
2017-12-28 18:14:33 +01:00
|
|
|
maxLines := 10
|
|
|
|
description := strings.TrimSpace(v.description)
|
|
|
|
if strings.Count(description, "\n") < maxLines {
|
|
|
|
return description
|
|
|
|
}
|
|
|
|
return strings.Join(strings.Split(description, "\n")[:maxLines], "\n") + "\n..."
|
|
|
|
}
|
|
|
|
|
2018-08-17 16:05:54 +02:00
|
|
|
func (v *YoutubeVideo) download() error {
|
2018-10-09 21:57:07 +02:00
|
|
|
videoPath := v.getFullPath()
|
2017-12-28 18:14:33 +01:00
|
|
|
|
2018-07-24 02:01:35 +02:00
|
|
|
err := os.Mkdir(v.videoDir(), 0750)
|
2018-06-09 01:14:55 +02:00
|
|
|
if err != nil && !strings.Contains(err.Error(), "file exists") {
|
|
|
|
return errors.Wrap(err, 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = os.Stat(videoPath)
|
2017-12-28 18:14:33 +01:00
|
|
|
if err != nil && !os.IsNotExist(err) {
|
|
|
|
return err
|
|
|
|
} else if err == nil {
|
|
|
|
log.Debugln(v.id + " already exists at " + videoPath)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-03-13 22:48:01 +01:00
|
|
|
videoUrl := "https://www.youtube.com/watch?v=" + v.id
|
|
|
|
videoInfo, err := ytdl.GetVideoInfo(videoUrl)
|
2017-12-28 18:14:33 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-03-13 22:48:01 +01:00
|
|
|
|
2018-09-18 23:28:25 +02:00
|
|
|
codec := []string{"H.264"}
|
|
|
|
ext := []string{"mp4"}
|
|
|
|
|
|
|
|
//Filter requires a [] interface{}
|
|
|
|
codecFilter := make([]interface{}, len(codec))
|
|
|
|
for i, v := range codec {
|
|
|
|
codecFilter[i] = v
|
2017-12-28 18:14:33 +01:00
|
|
|
}
|
2018-03-13 22:48:01 +01:00
|
|
|
|
2018-09-18 23:28:25 +02:00
|
|
|
//Filter requires a [] interface{}
|
|
|
|
extFilter := make([]interface{}, len(ext))
|
|
|
|
for i, v := range ext {
|
|
|
|
extFilter[i] = v
|
|
|
|
}
|
|
|
|
|
|
|
|
formats := videoInfo.Formats.Filter(ytdl.FormatVideoEncodingKey, codecFilter).Filter(ytdl.FormatExtensionKey, extFilter)
|
|
|
|
if len(formats) == 0 {
|
|
|
|
return errors.Err("no compatible format available for this video")
|
|
|
|
}
|
|
|
|
maxRetryAttempts := 5
|
2018-10-11 23:21:05 +02:00
|
|
|
if videoInfo.Duration.Hours() > 2 {
|
|
|
|
return errors.Err("video is too long to process")
|
|
|
|
}
|
|
|
|
|
2018-09-18 23:28:25 +02:00
|
|
|
for i := 0; i < len(formats) && i < maxRetryAttempts; i++ {
|
|
|
|
formatIndex := i
|
|
|
|
if i == maxRetryAttempts-1 {
|
|
|
|
formatIndex = len(formats) - 1
|
|
|
|
}
|
|
|
|
var downloadedFile *os.File
|
|
|
|
downloadedFile, err = os.Create(videoPath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
err = videoInfo.Download(formats[formatIndex], downloadedFile)
|
|
|
|
downloadedFile.Close()
|
|
|
|
if err != nil {
|
2018-10-03 02:51:42 +02:00
|
|
|
//delete the video and ignore the error
|
|
|
|
_ = v.delete()
|
2018-09-18 23:28:25 +02:00
|
|
|
break
|
|
|
|
}
|
2018-10-09 21:57:07 +02:00
|
|
|
fi, err := os.Stat(v.getFullPath())
|
2018-09-18 23:28:25 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
videoSize := fi.Size()
|
|
|
|
v.size = &videoSize
|
|
|
|
|
|
|
|
if videoSize > v.maxVideoSize {
|
|
|
|
//delete the video and ignore the error
|
|
|
|
_ = v.delete()
|
|
|
|
err = errors.Err("file is too big and there is no other format available")
|
|
|
|
} else {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return err
|
2017-12-28 18:14:33 +01:00
|
|
|
}
|
|
|
|
|
2018-08-17 16:05:54 +02:00
|
|
|
func (v *YoutubeVideo) videoDir() string {
|
2018-07-24 02:01:35 +02:00
|
|
|
return v.dir + "/" + v.id
|
|
|
|
}
|
|
|
|
|
2018-08-17 16:05:54 +02:00
|
|
|
func (v *YoutubeVideo) delete() error {
|
2018-10-09 21:57:07 +02:00
|
|
|
videoPath := v.getFullPath()
|
2018-04-25 20:56:26 +02:00
|
|
|
err := os.Remove(videoPath)
|
|
|
|
if err != nil {
|
2018-07-17 20:58:47 +02:00
|
|
|
log.Errorln(errors.Prefix("delete error", err))
|
2018-04-25 20:56:26 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
log.Debugln(v.id + " deleted from disk (" + videoPath + ")")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-08-17 16:05:54 +02:00
|
|
|
func (v *YoutubeVideo) triggerThumbnailSave() error {
|
2017-12-28 18:14:33 +01:00
|
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
|
|
|
|
|
|
params, err := json.Marshal(map[string]string{"videoid": v.id})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
request, err := http.NewRequest(http.MethodPut, "https://jgp4g1qoud.execute-api.us-east-1.amazonaws.com/prod/thumbnail", bytes.NewBuffer(params))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
response, err := client.Do(request)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer response.Body.Close()
|
|
|
|
|
|
|
|
contents, err := ioutil.ReadAll(response.Body)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var decoded struct {
|
2018-09-05 19:54:33 +02:00
|
|
|
Error int `json:"error"`
|
|
|
|
Url string `json:"url,omitempty"`
|
|
|
|
Message string `json:"message,omitempty"`
|
2017-12-28 18:14:33 +01:00
|
|
|
}
|
|
|
|
err = json.Unmarshal(contents, &decoded)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-09-05 19:54:33 +02:00
|
|
|
if decoded.Error != 0 {
|
|
|
|
return errors.Err("error creating thumbnail: " + decoded.Message)
|
2017-12-28 18:14:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func strPtr(s string) *string { return &s }
|
|
|
|
|
2018-09-18 22:57:25 +02:00
|
|
|
func (v *YoutubeVideo) publish(daemon *jsonrpc.Client, claimAddress string, amount float64, channelID string, namer *namer.Namer) (*SyncSummary, error) {
|
2018-08-03 23:19:36 +02:00
|
|
|
if channelID == "" {
|
|
|
|
return nil, errors.Err("a claim_id for the channel wasn't provided") //TODO: this is probably not needed?
|
|
|
|
}
|
2017-12-28 18:14:33 +01:00
|
|
|
options := jsonrpc.PublishOptions{
|
2018-05-26 02:43:16 +02:00
|
|
|
Title: &v.title,
|
|
|
|
Author: &v.channelTitle,
|
|
|
|
Description: strPtr(v.getAbbrevDescription() + "\nhttps://www.youtube.com/watch?v=" + v.id),
|
|
|
|
Language: strPtr("en"),
|
|
|
|
ClaimAddress: &claimAddress,
|
|
|
|
Thumbnail: strPtr("https://berk.ninja/thumbnails/" + v.id),
|
|
|
|
License: strPtr("Copyrighted (contact author)"),
|
|
|
|
ChangeAddress: &claimAddress,
|
2018-08-03 23:19:36 +02:00
|
|
|
ChannelID: &channelID,
|
2017-12-28 18:14:33 +01:00
|
|
|
}
|
2018-09-18 21:20:34 +02:00
|
|
|
|
2018-10-09 21:57:07 +02:00
|
|
|
return publishAndRetryExistingNames(daemon, v.title, v.getFullPath(), amount, options, namer)
|
2017-12-28 18:14:33 +01:00
|
|
|
}
|
|
|
|
|
2018-08-17 16:05:54 +02:00
|
|
|
func (v *YoutubeVideo) Size() *int64 {
|
2018-08-14 17:09:23 +02:00
|
|
|
return v.size
|
|
|
|
}
|
|
|
|
|
2018-09-18 22:57:25 +02:00
|
|
|
func (v *YoutubeVideo) Sync(daemon *jsonrpc.Client, claimAddress string, amount float64, channelID string, maxVideoSize int, namer *namer.Namer) (*SyncSummary, error) {
|
2018-09-18 23:28:25 +02:00
|
|
|
v.maxVideoSize = int64(maxVideoSize) * 1024 * 1024
|
2017-12-28 18:14:33 +01:00
|
|
|
//download and thumbnail can be done in parallel
|
2018-02-13 18:47:05 +01:00
|
|
|
err := v.download()
|
2017-12-28 18:14:33 +01:00
|
|
|
if err != nil {
|
2018-07-21 01:56:36 +02:00
|
|
|
return nil, errors.Prefix("download error", err)
|
2017-12-28 18:14:33 +01:00
|
|
|
}
|
|
|
|
log.Debugln("Downloaded " + v.id)
|
|
|
|
|
2018-02-13 18:47:05 +01:00
|
|
|
err = v.triggerThumbnailSave()
|
2017-12-28 18:14:33 +01:00
|
|
|
if err != nil {
|
2018-07-21 01:56:36 +02:00
|
|
|
return nil, errors.Prefix("thumbnail error", err)
|
2017-12-28 18:14:33 +01:00
|
|
|
}
|
|
|
|
log.Debugln("Created thumbnail for " + v.id)
|
|
|
|
|
2018-09-18 21:20:34 +02:00
|
|
|
summary, err := v.publish(daemon, claimAddress, amount, channelID, namer)
|
2018-06-06 23:47:28 +02:00
|
|
|
//delete the video in all cases (and ignore the error)
|
|
|
|
_ = v.delete()
|
2018-04-25 20:56:26 +02:00
|
|
|
|
2018-10-03 02:51:42 +02:00
|
|
|
return summary, errors.Prefix("publish error", err)
|
2017-12-28 18:14:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// sorting videos
|
|
|
|
//type ByPublishedAt []YoutubeVideo
|
|
|
|
//
|
|
|
|
//func (a ByPublishedAt) Len() int { return len(a) }
|
|
|
|
//func (a ByPublishedAt) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
|
|
//func (a ByPublishedAt) Less(i, j int) bool { return a[i].publishedAt.Before(a[j].publishedAt) }
|
|
|
|
//
|
|
|
|
//type ByPlaylistPosition []YoutubeVideo
|
|
|
|
//
|
|
|
|
//func (a ByPlaylistPosition) Len() int { return len(a) }
|
|
|
|
//func (a ByPlaylistPosition) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
|
|
//func (a ByPlaylistPosition) Less(i, j int) bool { return a[i].playlistPosition < a[j].playlistPosition }
|