From d6fd773efd5af8eb04f4848729880676b1634d69 Mon Sep 17 00:00:00 2001 From: Ian Bishop <1296987+porjo@users.noreply.github.com> Date: Sat, 23 Nov 2024 22:04:19 +1000 Subject: [PATCH] Add recordingDate flag --- README.md | 4 +- cmd/youtubeuploader/main.go | 4 ++ files.go | 15 ++++++ test/upload_test.go | 101 ++++++++++++++++++++++++++++++++---- 4 files changed, 114 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ac0d87a..580d8da 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,8 @@ Usage: suppress progress indicator -ratelimit int rate limit upload in Kbps. No limit by default + -recordingDate value + recording date e.g. 2024-11-23 -secrets string Client Secrets configuration (default "client_secrets.json") -sendFilename @@ -139,7 +141,7 @@ Video title, description etc can specified via the command line flags or via a J - all fields are optional - use `\n` in the description to insert newlines - times can be provided in one of two formats: `yyyy-mm-dd` (UTC) or `yyyy-mm-ddThh:mm:ss+zz:zz` -- any values supplied via command line will take precedence +- any values supplied via `-metaJSON` will take precedence over flags ## Credit diff --git a/cmd/youtubeuploader/main.go b/cmd/youtubeuploader/main.go index 06de834..f2ef6be 100644 --- a/cmd/youtubeuploader/main.go +++ b/cmd/youtubeuploader/main.go @@ -53,8 +53,11 @@ func main() { var err error var playlistIDs arrayFlags + var recordingDate yt.Date flag.Var(&playlistIDs, "playlistID", "playlist ID to add the video to. Can be used multiple times") + flag.Var(&recordingDate, "recordingDate", "recording date e.g. 2024-11-23") + filename := flag.String("filename", "", "video filename. Can be a URL. Read from stdin with '-'") thumbnail := flag.String("thumbnail", "", "thumbnail filename. Can be a URL") caption := flag.String("caption", "", "caption filename. Can be a URL") @@ -98,6 +101,7 @@ func main() { NotifySubscribers: *notifySubscribers, SendFileName: *sendFileName, PlaylistIDs: playlistIDs, + RecordingDate: recordingDate, } config.Logger = utils.NewLogger(*debug) diff --git a/files.go b/files.go index 95431c4..04a73a7 100644 --- a/files.go +++ b/files.go @@ -61,6 +61,7 @@ type Config struct { Chunksize int NotifySubscribers bool SendFileName bool + RecordingDate Date Logger utils.Logger } @@ -178,6 +179,10 @@ func LoadVideoMeta(config Config, video *youtube.Video) (*VideoMeta, error) { video.Snippet.DefaultAudioLanguage = config.Language } + if video.RecordingDetails.RecordingDate == "" && !config.RecordingDate.IsZero() { + video.RecordingDetails.RecordingDate = config.RecordingDate.UTC().Format(ytDateLayout) + } + // combine cli flag playistIDs and metaJSON playlistIDs. Remove any duplicates playlistIDs := slices.Concat(config.PlaylistIDs, videoMeta.PlaylistIDs) slices.Sort(playlistIDs) @@ -261,6 +266,16 @@ func Open(filename string, mediaType MediaType) (io.ReadCloser, int, error) { func (d *Date) UnmarshalJSON(b []byte) (err error) { s := string(b) s = s[1 : len(s)-1] + err = d.parse(s) + return +} + +func (d *Date) Set(s string) (err error) { + err = d.parse(s) + return +} + +func (d *Date) parse(s string) (err error) { // support ISO 8601 date only, and date + time if strings.ContainsAny(s, ":") { d.Time, err = time.Parse(inputDatetimeLayout, s) diff --git a/test/upload_test.go b/test/upload_test.go index 1042c1d..000caea 100644 --- a/test/upload_test.go +++ b/test/upload_test.go @@ -20,6 +20,9 @@ import ( "fmt" "io" "log" + "log/slog" + "mime" + "mime/multipart" "net/http" "net/http/httptest" "net/url" @@ -50,6 +53,10 @@ var ( config yt.Config transport *mockTransport + + recordingDate yt.Date + + logger *slog.Logger ) type mockTransport struct { @@ -62,7 +69,7 @@ type mockReader struct { } func (m *mockTransport) RoundTrip(r *http.Request) (*http.Response, error) { - fmt.Printf("%s URL %s\n", r.Method, r.URL.String()) + logger.Info("roundtrip", "method", r.Method, "URL", r.URL.String()) r.URL.Scheme = m.url.Scheme r.URL.Host = m.url.Host @@ -87,16 +94,26 @@ func (m *mockReader) Read(p []byte) (int, error) { func TestMain(m *testing.M) { + logger = slog.Default() + testServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // be sure to read the request body otherwise the client gets confused - _, err := io.Copy(io.Discard, r.Body) + l := logger.With("src", "httptest") + + video, err := handleVideoPost(r, l) if err != nil { - log.Printf("Error reading body: %v", err) - http.Error(w, "can't read body", http.StatusBadRequest) - return + http.Error(w, err.Error(), http.StatusBadRequest) + } + + if video != nil { + recDateIn, err := time.Parse(time.RFC3339Nano, video.RecordingDetails.RecordingDate) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + if recDateIn.Equal(recordingDate.Time) { + http.Error(w, "Date didn't match", http.StatusBadRequest) + } } - // log.Printf("Mock server: request body length %d", len(body)) w.Header().Set("Content-Type", "application/json") switch r.Host { @@ -110,7 +127,6 @@ func TestMain(m *testing.M) { } videoJ, err := json.Marshal(video) if err != nil { - fmt.Printf("json marshall error %s\n", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -133,7 +149,6 @@ func TestMain(m *testing.M) { } playlistJ, err := json.Marshal(playlistResponse) if err != nil { - fmt.Printf("json marshall error %s\n", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -158,6 +173,9 @@ func TestMain(m *testing.M) { config.Logger = utils.NewLogger(false) config.Filename = "test.mp4" config.PlaylistIDs = []string{"xxxx", "yyyy"} + recordingDate = yt.Date{} + recordingDate.Time = time.Now() + config.RecordingDate = recordingDate ret := m.Run() @@ -198,3 +216,68 @@ func TestRateLimit(t *testing.T) { } } + +func handleVideoPost(r *http.Request, l *slog.Logger) (*youtube.Video, error) { + + if r.Method != http.MethodPost { + l.Info("not POST, skipping") + return nil, nil + } + // Parse the Content-Type header + contentType := r.Header.Get("Content-Type") + if contentType == "" { + return nil, fmt.Errorf("Missing Content-Type header") + } + + // Parse the media type and boundary + mediaType, params, err := mime.ParseMediaType(contentType) + if err != nil { + return nil, err + } + + if mediaType != "multipart/related" { + l.Info("not multipart, skipping") + return nil, nil + } + + boundary, ok := params["boundary"] + if !ok { + return nil, fmt.Errorf("Missing boundary parameter") + } + + // Parse the multipart form + mr := multipart.NewReader(r.Body, boundary) + + video := &youtube.Video{} + + // Iterate through the parts + for { + part, err := mr.NextPart() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + contentType := part.Header.Get("Content-Type") + switch contentType { + case "application/json": + // Parse JSON part + err := json.NewDecoder(part).Decode(video) + if err != nil { + return nil, err + } + case "application/octet-stream": + // Read binary data part + _, err = io.Copy(io.Discard, part) + if err != nil { + return nil, err + } + default: + // Ignore other content types + } + } + + return video, nil +}