183 private string _title =
string.Empty;
184 private string _description =
string.Empty;
185 private string[] _tags =
new string[0];
186 private YouTubePrivacy _privacy = YouTubePrivacy.Private;
187 private YouTubeCategory _category = YouTubeCategory.ScienceTechnology;
188 private List<string> _videoIds =
new List<string>();
189 private int _pollIntervalSecs = 60;
190 private TextBoxBase _outputControl;
191 private ManualResetEventSlim _completionEvent =
new ManualResetEventSlim(
false);
192 private string _playlistId =
null;
193 private List<string> _lookupVideoIds =
new List<string>();
194 private List<string> _searchTerms =
new List<string>();
195 private int _position = -1;
198 private bool _titleGiven;
199 private bool _descriptionGiven;
200 private bool _tagsGiven;
201 private bool _privacyGiven;
202 private bool _categoryGiven;
204 private const string InitiationUrl =
"https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&part=snippet,status";
205 private const string VideoStatusUrl =
"https://www.googleapis.com/youtube/v3/videos?part=status&id=";
206 private const string VideoUpdateUrl =
"https://www.googleapis.com/youtube/v3/videos?part=status";
207 private const string PlaylistCreateUrl =
"https://www.googleapis.com/youtube/v3/playlists?part=snippet,status";
208 private const string PlaylistItemUrl =
"https://www.googleapis.com/youtube/v3/playlistItems?part=snippet";
209 private const string VideoInfoBaseUrl =
"https://www.googleapis.com/youtube/v3/videos?part=snippet,status&id=";
210 private const string SearchBaseUrl =
"https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&forMine=true&maxResults=50&q=";
211 private const string MyChannelUrl =
"https://www.googleapis.com/youtube/v3/channels?part=contentDetails&mine=true";
212 private const string PlaylistItemsListUrl =
"https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=";
213 private const string PlaylistItemDeleteUrl =
"https://www.googleapis.com/youtube/v3/playlistItems?id=";
214 private const string MyPlaylistsUrl =
"https://www.googleapis.com/youtube/v3/playlists?part=snippet,status,contentDetails&mine=true&maxResults=50";
215 private const string VideoDeleteUrl =
"https://www.googleapis.com/youtube/v3/videos?id=";
216 private const string ThumbnailSetUrl =
"https://www.googleapis.com/upload/youtube/v3/thumbnails/set?uploadType=media&videoId=";
219 private const int MaxUnansweredStatusChecks = 5;
221 internal GPALYouTube() { }
227 if (credentials ==
null)
232 _credentials = credentials;
239 _title = title ??
string.Empty;
246 _description = description ??
string.Empty;
247 _descriptionGiven =
true;
253 _tags = tags ??
new string[0];
261 _privacyGiven =
true;
265 public IGPALYouTube WithCategory(YouTubeCategory category)
267 _category = category;
268 _categoryGiven =
true;
274 if (_credentials ==
null)
276 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Credentials must be set before Upload.",
this, GPALObjectType.YouTube);
281 if (
string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
283 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"File not found: [{filePath}]",
this, GPALObjectType.YouTube);
289 _credentials.FetchAccessToken(out
string token);
290 if (
string.IsNullOrEmpty(token))
292 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"Failed to fetch access token.",
this, GPALObjectType.YouTube);
296 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Uploading: [{Path.GetFileName(filePath)}]",
this, GPALObjectType.YouTube);
298 string uploadUri = InitiateResumableUpload(token, filePath);
299 if (
string.IsNullOrEmpty(uploadUri))
return null;
301 string videoId = UploadVideoBytes(uploadUri, filePath);
302 if (
string.IsNullOrEmpty(videoId))
return null;
304 string url = $
"https://youtu.be/{videoId}";
306 if (!
string.IsNullOrEmpty(_playlistId))
307 AddVideoToPlaylist(token, _playlistId, videoId, -1, GPALEventType.ERROR);
312 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION,
"YouTube upload failed.",
this, GPALObjectType.YouTube, ex);
321 if (result ==
null ||
string.IsNullOrEmpty(result.VideoId))
323 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"WithVideo: null or missing VideoId.",
this, GPALObjectType.YouTube);
326 _videoIds.Add(result.VideoId);
330 public IGPALYouTube WithVideos(IEnumerable<YouTubeUploadResult> results)
334 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"WithVideos: null results list.",
this, GPALObjectType.YouTube);
337 foreach (var r
in results)
338 if (r !=
null && !
string.IsNullOrEmpty(r.VideoId))
339 _videoIds.Add(r.VideoId);
343 public IGPALYouTube WaitForProcessing(
int pollIntervalSeconds = 60)
345 _pollIntervalSecs = Math.Max(10, pollIntervalSeconds);
359 if (_credentials ==
null)
361 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"ScheduleAt: credentials not set.",
this, GPALObjectType.YouTube);
362 _completionEvent.Set();
364 else if (_videoIds.Count == 0)
366 GPAL.
PublishSimpleEvent(GPALEventType.ERROR,
"ScheduleAt: no videos targeted. Call WithVideo or WithVideos first.",
this, GPALObjectType.YouTube);
367 _completionEvent.Set();
372 var videoIds =
new List<string>(_videoIds);
373 var creds = _credentials;
374 var intervalMs = _pollIntervalSecs * 1000;
378 var thread =
new Thread(() =>
382 PollAndSchedule(creds, videoIds, premiereTime, intervalMs);
386 string errMsg =
"ScheduleAt: polling stopped before all videos were scheduled.";
388 AppendToOutput(errMsg);
392 _completionEvent.Set();
395 thread.IsBackground =
true;
399 $
"ScheduleAt started in background for [{videoIds.Count}] video(s). Poll interval: [{_pollIntervalSecs}]s.",
400 this, GPALObjectType.YouTube);
415 _outputControl = control;
426 _completionEvent.Wait();
429 private void AppendToOutput(
string message)
431 if (_outputControl ==
null)
return;
432 if (_outputControl.InvokeRequired)
433 _outputControl.BeginInvoke(
new Action(() => AppendToOutput(message)));
435 _outputControl.AppendText(message + Environment.NewLine);
440 private void PollAndSchedule(
ICredentials creds, List<string> videoIds, PremiereTime premiereTime,
int intervalMs)
442 var pending =
new HashSet<string>(videoIds);
443 var lastStatus =
new Dictionary<string, string>();
446 string openingMsg = $
"Suppressing repeat status messages for: {string.Join(",
", videoIds)}. Will report when processing completes for each.";
448 AppendToOutput(openingMsg);
450 while (pending.Count > 0)
452 creds.FetchAccessToken(out
string token);
454 var justProcessed =
new List<string>();
456 foreach (
string id in new List<string>(pending))
458 string status = GetUploadStatus(token,
id);
461 if (!lastStatus.TryGetValue(
id, out prev) || prev != status)
463 string msg = $
"Video {id}: {status}";
466 lastStatus[id] = status;
469 if (status ==
"processed")
471 justProcessed.Add(
id);
473 else if (status ==
"failed" || status ==
"rejected" || status ==
"deleted")
475 string errMsg = $
"Video {id} cannot be scheduled: {status}.";
476 GPAL.PublishSimpleEvent(GPALEventType.ERROR, errMsg,
this, GPALObjectType.YouTube);
477 AppendToOutput(errMsg);
478 justProcessed.Add(
id);
482 foreach (
string id in justProcessed)
486 if (lastStatus.TryGetValue(
id, out finalStatus) && finalStatus ==
"processed")
488 DateTime pt = premiereTime.Resolve();
489 SetPremiere(token,
id, pt);
494 if (pending.Count > 0)
495 Thread.Sleep(intervalMs);
500 string doneMsg = 0 == scheduled
501 ? $
"No videos were scheduled. None of the [{videoIds.Count}] finished processing."
502 : $
"Scheduled [{scheduled}] of [{videoIds.Count}] video(s).";
504 GPAL.PublishSimpleEvent(0 == scheduled ? GPALEventType.WARNING : GPALEventType.INFO, doneMsg,
this, GPALObjectType.YouTube);
505 AppendToOutput(doneMsg);
508 private void SetPremiere(
string token,
string videoId, DateTime premiereUtc)
512 string body =
"{\"id\":" + JsonString(videoId)
513 +
",\"status\":{\"privacyStatus\":\"private\",\"publishAt\":" + JsonString(premiereUtc.ToString(
"o")) +
"}}";
514 byte[] bodyBytes = Encoding.UTF8.GetBytes(body);
516 var req = (HttpWebRequest)WebRequest.Create(VideoUpdateUrl);
518 req.Headers[
"Authorization"] =
"Bearer " + token;
519 req.ContentType =
"application/json";
520 req.ContentLength = bodyBytes.Length;
522 using (var s = req.GetRequestStream())
523 s.Write(bodyBytes, 0, bodyBytes.Length);
525 using ((HttpWebResponse)req.GetResponse()) { }
527 string schedMsg = $
"Premiere scheduled: https://youtu.be/{videoId} at {premiereUtc:o}";
528 GPAL.PublishSimpleEvent(GPALEventType.INFO, schedMsg,
this, GPALObjectType.YouTube);
529 AppendToOutput(schedMsg);
533 string errMsg = $
"SetPremiere failed for {videoId}.";
534 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, errMsg,
this, GPALObjectType.YouTube, ex);
535 AppendToOutput(errMsg);
539 private string GetUploadStatus(
string token,
string videoId)
543 var req = (HttpWebRequest)WebRequest.Create(VideoStatusUrl + videoId);
545 req.Headers[
"Authorization"] =
"Bearer " + token;
547 using (var resp = (HttpWebResponse)req.GetResponse())
548 using (var reader =
new StreamReader(resp.GetResponseStream()))
550 string responseBody = reader.ReadToEnd();
551 using (var doc = JsonDocument.Parse(responseBody))
553 if (doc.RootElement.TryGetProperty(
"items", out var items)
554 && items.GetArrayLength() > 0
555 && items[0].TryGetProperty(
"status", out var status)
556 && status.TryGetProperty(
"uploadStatus", out var uploadStatus))
558 return uploadStatus.GetString();
565 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"GetUploadStatus failed for [{videoId}].",
this, GPALObjectType.YouTube, ex);
572 private string InitiateResumableUpload(
string token,
string filePath)
574 long fileSize =
new FileInfo(filePath).Length;
575 byte[] body = Encoding.UTF8.GetBytes(BuildMetadataJson());
577 var req = (HttpWebRequest)WebRequest.Create(InitiationUrl);
579 req.Headers[
"Authorization"] =
"Bearer " + token;
580 req.ContentType =
"application/json; charset=UTF-8";
581 req.Headers[
"X-Upload-Content-Type"] =
"video/mp4";
582 req.Headers[
"X-Upload-Content-Length"] = fileSize.ToString();
583 req.ContentLength = body.Length;
585 using (var s = req.GetRequestStream())
586 s.Write(body, 0, body.Length);
590 using (var resp = (HttpWebResponse)req.GetResponse())
592 string uri = resp.Headers[
"Location"];
593 GPAL.PublishSimpleEvent(GPALEventType.INFO,
"Resumable upload session started.",
this, GPALObjectType.YouTube);
597 catch (WebException wex) when (wex.Response is HttpWebResponse errResp)
599 using (var reader =
new System.IO.StreamReader(errResp.GetResponseStream()))
601 string detail = reader.ReadToEnd();
602 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Upload initiation failed ([{(int)errResp.StatusCode}]): [{detail}]",
this, GPALObjectType.YouTube);
608 private string UploadVideoBytes(
string uploadUri,
string filePath)
610 long fileSize =
new FileInfo(filePath).Length;
612 var req = (HttpWebRequest)WebRequest.Create(uploadUri);
614 req.ContentType =
"video/mp4";
615 req.ContentLength = fileSize;
616 req.Timeout = 3_600_000;
617 req.AllowWriteStreamBuffering =
false;
619 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Sending [{fileSize:N0}] bytes...",
this, GPALObjectType.YouTube);
621 using (var reqStream = req.GetRequestStream())
622 using (var fs = File.OpenRead(filePath))
624 byte[] buf =
new byte[256 * 1024];
626 while ((read = fs.Read(buf, 0, buf.Length)) > 0)
627 reqStream.Write(buf, 0, read);
630 using (var resp = (HttpWebResponse)req.GetResponse())
631 using (var reader =
new StreamReader(resp.GetResponseStream()))
633 string responseBody = reader.ReadToEnd();
634 using (var doc = JsonDocument.Parse(responseBody))
636 if (doc.RootElement.TryGetProperty(
"id", out var idEl))
637 return idEl.GetString();
641 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"Upload response missing video ID.",
this, GPALObjectType.YouTube);
645 private string PrivacyString()
649 case YouTubePrivacy.Public:
return "public";
650 case YouTubePrivacy.Unlisted:
return "unlisted";
651 default:
return "private";
655 private string BuildMetadataJson()
657 string privacyStr = PrivacyString();
659 var tags =
new StringBuilder(
"[");
660 for (
int i = 0; i < _tags.Length; i++)
662 if (i > 0) tags.Append(
",");
663 tags.Append(JsonString(_tags[i]));
667 return "{\"snippet\":{\"title\":" + JsonString(_title)
668 +
",\"description\":" + JsonString(_description)
669 +
",\"tags\":" + tags
670 +
",\"categoryId\":" + JsonString(((
int)_category).ToString())
671 +
"},\"status\":{\"privacyStatus\":" + JsonString(privacyStr) +
"}}";
674 private static string JsonString(
string s)
676 if (s ==
null)
return "\"\"";
677 return "\"" + s.Replace(
"\\",
"\\\\")
678 .Replace(
"\"",
"\\\"")
679 .Replace(
"\n",
"\\n")
680 .Replace(
"\r",
"\\r")
681 .Replace(
"\t",
"\\t")
687 public IGPALYouTube WithPlaylist(
string playlistId)
689 _playlistId = playlistId;
693 public YouTubePlaylistResult CreatePlaylist()
695 if (_credentials ==
null)
697 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"CreatePlaylist: credentials not set.",
this, GPALObjectType.YouTube);
702 _credentials.FetchAccessToken(out
string token);
703 if (
string.IsNullOrEmpty(token))
705 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"CreatePlaylist: failed to fetch access token.",
this, GPALObjectType.YouTube);
709 string body =
"{\"snippet\":{\"title\":" + JsonString(_title)
710 +
",\"description\":" + JsonString(_description)
711 +
"},\"status\":{\"privacyStatus\":" + JsonString(PrivacyString()) +
"}}";
712 byte[] bodyBytes = Encoding.UTF8.GetBytes(body);
714 var req = (HttpWebRequest)WebRequest.Create(PlaylistCreateUrl);
716 req.Headers[
"Authorization"] =
"Bearer " + token;
717 req.ContentType =
"application/json";
718 req.ContentLength = bodyBytes.Length;
720 using (var s = req.GetRequestStream())
721 s.Write(bodyBytes, 0, bodyBytes.Length);
723 using (var resp = (HttpWebResponse)req.GetResponse())
724 using (var reader =
new StreamReader(resp.GetResponseStream()))
726 string responseBody = reader.ReadToEnd();
727 using (var doc = JsonDocument.Parse(responseBody))
729 if (doc.RootElement.TryGetProperty(
"id", out var idEl))
731 string playlistId = idEl.GetString();
732 GPAL.PublishSimpleEvent(GPALEventType.INFO,
733 $
"Playlist created: https://www.youtube.com/playlist?list=[{playlistId}]",
this, GPALObjectType.YouTube);
734 return new YouTubePlaylistResult(playlistId);
738 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"CreatePlaylist: response missing playlist ID.",
this, GPALObjectType.YouTube);
743 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
"CreatePlaylist failed.",
this, GPALObjectType.YouTube, ex);
748 public IGPALYouTube AddToPlaylist(
string playlistId)
750 if (_credentials ==
null)
752 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"AddToPlaylist: credentials not set.",
this, GPALObjectType.YouTube);
756 var videoIds =
new List<string>(_videoIds);
757 foreach (
string id in _lookupVideoIds)
758 if (!videoIds.Contains(
id))
761 if (videoIds.Count == 0)
763 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"AddToPlaylist: no videos. Call WithVideo, WithVideos or WithVideoId first.",
this, GPALObjectType.YouTube);
766 _credentials.FetchAccessToken(out
string token);
769 int position = _position;
770 foreach (
string videoId
in videoIds)
772 AddVideoToPlaylist(token, playlistId, videoId, position, GPALEventType.ERROR);
773 if (position >= 0) position++;
784 _position = position;
790 private bool AddVideoToPlaylist(
string token,
string playlistId,
string videoId,
int position, GPALEventType refusedAs)
796 string positionJson = position >= 0 ?
",\"position\":" + position :
string.Empty;
797 string body =
"{\"snippet\":{\"playlistId\":" + JsonString(playlistId)
799 +
",\"resourceId\":{\"kind\":\"youtube#video\",\"videoId\":" + JsonString(videoId) +
"}}}";
801 added = CallApi(
"POST", PlaylistItemUrl, token,
"application/json", Encoding.UTF8.GetBytes(body), out
string answer);
805 $
"Added [{videoId}] to playlist [{playlistId}].",
this, GPALObjectType.YouTube);
808 $
"Could not add [{videoId}] to playlist [{playlistId}]: [{answer}]",
this, GPALObjectType.YouTube);
813 $
"AddVideoToPlaylist failed for [{videoId}].",
this, GPALObjectType.YouTube, ex);
821 public IGPALYouTube WithVideoId(
string videoId)
823 if (!
string.IsNullOrEmpty(videoId))
824 _lookupVideoIds.Add(videoId);
828 public IGPALYouTube WithSearchTerms(
string query)
830 if (!
string.IsNullOrEmpty(query))
831 _searchTerms.Add(query);
835 public IGPALYouTube WithSearchTerms(IGPALGrid<string> grid)
837 if (grid ==
null)
return this;
838 foreach (var row
in grid)
839 if (row !=
null && row.Count > 0 && !
string.IsNullOrEmpty(row[0]))
840 _searchTerms.Add(row[0]);
844 public IGPALYouTube GetResults(out YouTubeVideoInfo info)
847 if (_credentials ==
null)
849 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"GetResults: credentials not set.",
this, GPALObjectType.YouTube);
852 if (_lookupVideoIds.Count == 0)
854 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
"GetResults: no video ID. Call WithVideoId first.",
this, GPALObjectType.YouTube);
857 _credentials.FetchAccessToken(out
string token);
858 var list = FetchVideoInfos(token, _lookupVideoIds);
859 info = list.Count > 0 ? list[0] :
null;
863 public IGPALYouTube GetResults(out List<YouTubeVideoInfo> results)
865 results =
new List<YouTubeVideoInfo>();
866 if (_credentials ==
null)
868 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"GetResults: credentials not set.",
this, GPALObjectType.YouTube);
871 _credentials.FetchAccessToken(out
string token);
873 if (_lookupVideoIds.Count > 0)
874 results.AddRange(FetchVideoInfos(token, _lookupVideoIds));
876 if (_searchTerms.Count > 0)
878 var seen =
new HashSet<string>();
879 foreach (var v
in results) seen.Add(v.VideoId);
880 foreach (
string term
in _searchTerms)
882 foreach (var v
in SearchVideos(token, term))
883 if (seen.Add(v.VideoId))
890 private List<YouTubeVideoInfo> FetchVideoInfos(
string token, List<string> videoIds)
892 var result =
new List<YouTubeVideoInfo>();
894 while (i < videoIds.Count)
896 var batch =
new StringBuilder();
897 int end = Math.Min(i + 50, videoIds.Count);
898 for (
int j = i; j < end; j++)
900 if (j > i) batch.Append(
",");
901 batch.Append(Uri.EscapeDataString(videoIds[j]));
907 var req = (HttpWebRequest)WebRequest.Create(VideoInfoBaseUrl + batch);
909 req.Headers[
"Authorization"] =
"Bearer " + token;
911 using (var resp = (HttpWebResponse)req.GetResponse())
912 using (var reader =
new StreamReader(resp.GetResponseStream()))
914 string responseBody = reader.ReadToEnd();
915 using (var doc = JsonDocument.Parse(responseBody))
917 if (!doc.RootElement.TryGetProperty(
"items", out var items))
continue;
918 foreach (var item
in items.EnumerateArray())
920 string videoId = item.TryGetProperty(
"id", out var idEl) ? idEl.GetString() :
null;
921 if (
string.IsNullOrEmpty(videoId))
continue;
923 string title =
string.Empty, description =
string.Empty;
924 string[] tags =
new string[0];
925 DateTime? publishedAt =
null;
926 string categoryId =
null, thumbnailUrl =
null;
927 YouTubePrivacy? privacy =
null;
929 if (item.TryGetProperty(
"status", out var statusEl)
930 && statusEl.TryGetProperty(
"privacyStatus", out var privacyEl))
931 privacy = PrivacyFrom(privacyEl.GetString());
933 if (item.TryGetProperty(
"snippet", out var snippet))
935 if (snippet.TryGetProperty(
"categoryId", out var c)) categoryId = c.GetString();
936 thumbnailUrl = BestThumbnail(snippet);
937 if (snippet.TryGetProperty(
"title", out var t)) title = t.GetString();
938 if (snippet.TryGetProperty(
"description", out var d)) description = d.GetString();
939 if (snippet.TryGetProperty(
"publishedAt", out var p)
940 && DateTime.TryParse(p.GetString(), out DateTime dt))
942 if (snippet.TryGetProperty(
"tags", out var tagsEl))
944 var tagList =
new List<string>();
945 foreach (var tag
in tagsEl.EnumerateArray())
946 tagList.Add(tag.GetString());
947 tags = tagList.ToArray();
950 result.Add(
new YouTubeVideoInfo(videoId, title, description, tags, publishedAt)
953 CategoryId = categoryId,
954 ThumbnailUrl = thumbnailUrl,
962 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
"FetchVideoInfos failed.",
this, GPALObjectType.YouTube, ex);
968 private List<YouTubeVideoInfo> SearchVideos(
string token,
string query)
970 var result =
new List<YouTubeVideoInfo>();
973 var req = (HttpWebRequest)WebRequest.Create(SearchBaseUrl + Uri.EscapeDataString(query));
975 req.Headers[
"Authorization"] =
"Bearer " + token;
977 using (var resp = (HttpWebResponse)req.GetResponse())
978 using (var reader =
new StreamReader(resp.GetResponseStream()))
980 string responseBody = reader.ReadToEnd();
981 using (var doc = JsonDocument.Parse(responseBody))
983 if (!doc.RootElement.TryGetProperty(
"items", out var items))
return result;
984 foreach (var item
in items.EnumerateArray())
986 string videoId =
null;
987 if (item.TryGetProperty(
"id", out var idObj)
988 && idObj.TryGetProperty(
"videoId", out var vidEl))
989 videoId = vidEl.GetString();
990 if (
string.IsNullOrEmpty(videoId))
continue;
992 string title =
string.Empty, description =
string.Empty;
993 DateTime? publishedAt =
null;
995 if (item.TryGetProperty(
"snippet", out var snippet))
997 if (snippet.TryGetProperty(
"title", out var t)) title = t.GetString();
998 if (snippet.TryGetProperty(
"description", out var d)) description = d.GetString();
999 if (snippet.TryGetProperty(
"publishedAt", out var p)
1000 && DateTime.TryParse(p.GetString(), out DateTime dt))
1003 result.Add(
new YouTubeVideoInfo(videoId, title, description,
null, publishedAt));
1008 catch (Exception ex)
1010 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $
"SearchVideos failed for [{query}].",
this, GPALObjectType.YouTube, ex);
1023 var found =
new List<YouTubeVideoInfo>();
1024 string token = TokenFor(
"GetMyVideos");
1030 string uploadsId = UploadsPlaylistId(token);
1032 if (
null != uploadsId)
1034 var items =
new List<YouTubePlaylistItem>();
1036 if (ReadPlaylistItems(token, uploadsId, items, out
string refusal))
1038 var ids =
new List<string>();
1039 foreach (var item
in items)
1040 if (!
string.IsNullOrEmpty(item.VideoId))
1041 ids.Add(item.VideoId);
1043 found = FetchVideoInfos(token, ids);
1044 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Found [{found.Count}] video(s) on the channel.",
this, GPALObjectType.YouTube);
1048 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"GetMyVideos: could not read the uploads playlist: [{refusal}]",
this, GPALObjectType.YouTube);
1052 catch (Exception ex)
1054 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION,
"GetMyVideos failed.",
this, GPALObjectType.YouTube, ex);
1067 var found =
new List<YouTubePlaylistInfo>();
1068 string token = TokenFor(
"GetMyPlaylists");
1074 if (ReadMyPlaylists(token, found, out
string refusal))
1075 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Found [{found.Count}] playlist(s) on the channel.",
this, GPALObjectType.YouTube);
1077 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"GetMyPlaylists: could not read the playlists: [{refusal}]",
this, GPALObjectType.YouTube);
1079 catch (Exception ex)
1081 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION,
"GetMyPlaylists failed.",
this, GPALObjectType.YouTube, ex);
1094 var found =
new List<YouTubePlaylistItem>();
1096 if (
string.IsNullOrEmpty(_playlistId))
1098 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"GetPlaylistItems: no playlist. Call WithPlaylist first.",
this, GPALObjectType.YouTube);
1102 string token = TokenFor(
"GetPlaylistItems");
1108 if (ReadPlaylistItems(token, _playlistId, found, out
string refusal))
1109 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Found [{found.Count}] item(s) in playlist [{_playlistId}].",
this, GPALObjectType.YouTube);
1111 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"GetPlaylistItems: could not read playlist [{_playlistId}]: [{refusal}]",
this, GPALObjectType.YouTube);
1113 catch (Exception ex)
1115 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"GetPlaylistItems failed for [{_playlistId}].",
this, GPALObjectType.YouTube, ex);
1130 if (
string.IsNullOrEmpty(_playlistId))
1132 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"RemoveFromPlaylist: no playlist. Call WithPlaylist first.",
this, GPALObjectType.YouTube);
1134 else if (0 == _lookupVideoIds.Count)
1136 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"RemoveFromPlaylist: no video ID. Call WithVideoId first.",
this, GPALObjectType.YouTube);
1140 string token = TokenFor(
"RemoveFromPlaylist");
1146 var items =
new List<YouTubePlaylistItem>();
1148 if (!ReadPlaylistItems(token, _playlistId, items, out
string refusal))
1150 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"RemoveFromPlaylist: could not read playlist [{_playlistId}]: [{refusal}]",
this, GPALObjectType.YouTube);
1154 foreach (
string videoId
in _lookupVideoIds)
1158 foreach (var item
in items)
1159 if (videoId == item.VideoId && DeletePlaylistItem(token, item))
1163 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Removed [{videoId}] from playlist [{_playlistId}].",
this, GPALObjectType.YouTube);
1164 else if (!items.Exists(i => videoId == i.VideoId))
1165 GPAL.
PublishSimpleEvent(GPALEventType.WARNING, $
"RemoveFromPlaylist: [{videoId}] is not in playlist [{_playlistId}].",
this, GPALObjectType.YouTube);
1169 catch (Exception ex)
1171 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"RemoveFromPlaylist failed for [{_playlistId}].",
this, GPALObjectType.YouTube, ex);
1186 if (0 == _lookupVideoIds.Count)
1188 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"UpdateVideo: no video ID. Call WithVideoId first.",
this, GPALObjectType.YouTube);
1190 else if (!(_titleGiven || _descriptionGiven || _tagsGiven || _categoryGiven || _privacyGiven))
1192 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"UpdateVideo: nothing to change. Call WithTitle, WithDescription, WithTags, WithCategory or WithPrivacy first.",
this, GPALObjectType.YouTube);
1196 string token = TokenFor(
"UpdateVideo");
1200 foreach (
string videoId
in _lookupVideoIds)
1204 UpdateOneVideo(token, videoId);
1206 catch (Exception ex)
1208 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"UpdateVideo failed for [{videoId}].",
this, GPALObjectType.YouTube, ex);
1223 string imagePath = image?.
Filename;
1225 if (0 == _lookupVideoIds.Count)
1227 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"SetThumbnail: no video ID. Call WithVideoId first.",
this, GPALObjectType.YouTube);
1229 else if (
string.IsNullOrEmpty(imagePath) || !File.Exists(imagePath))
1231 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"SetThumbnail: file not found [{imagePath}]",
this, GPALObjectType.YouTube);
1235 string token = TokenFor(
"SetThumbnail");
1241 byte[] bytes = File.ReadAllBytes(imagePath);
1242 string contentType =
".png" == Path.GetExtension(imagePath).ToLowerInvariant() ?
"image/png" :
"image/jpeg";
1244 foreach (
string videoId
in _lookupVideoIds)
1245 SetThumbnailBytes(token, videoId, bytes, contentType, GPALEventType.ERROR);
1247 catch (Exception ex)
1249 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"SetThumbnail failed for [{imagePath}].",
this, GPALObjectType.YouTube, ex);
1262 if (0 == _lookupVideoIds.Count)
1264 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"DeleteVideo: no video ID. Call WithVideoId first.",
this, GPALObjectType.YouTube);
1268 string token = TokenFor(
"DeleteVideo");
1272 foreach (
string videoId
in _lookupVideoIds)
1276 if (CallApi(
"DELETE", VideoDeleteUrl + Uri.EscapeDataString(videoId), token,
null,
null, out
string answer))
1277 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Deleted video [{videoId}].",
this, GPALObjectType.YouTube);
1279 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to delete video [{videoId}]: [{answer}]",
this, GPALObjectType.YouTube);
1281 catch (Exception ex)
1283 GPAL.
PublishSimpleEvent(GPALEventType.EXCEPTION, $
"Failed to delete video [{videoId}].",
this, GPALObjectType.YouTube, ex);
1302 string filePath = newVideo?.
Filename;
1304 if (0 == _lookupVideoIds.Count)
1306 GPAL.
PublishSimpleEvent(GPALEventType.WARNING,
"ReplaceVideo: no video ID. Call WithVideoId first.",
this, GPALObjectType.YouTube);
1308 else if (
string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
1310 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"ReplaceVideo: file not found [{filePath}]",
this, GPALObjectType.YouTube);
1314 string token = TokenFor(
"ReplaceVideo");
1315 string oldId = _lookupVideoIds[0];
1321 List<YouTubeVideoInfo> found = FetchVideoInfos(token,
new List<string> { oldId });
1323 if (0 == found.Count)
1325 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"ReplaceVideo: video [{oldId}] was not found.",
this, GPALObjectType.YouTube);
1327 else if (FindPlaylistPlaces(token, oldId, out List<YouTubePlaylistItem> places))
1329 GPAL.
PublishSimpleEvent(GPALEventType.INFO, $
"Replacing [{oldId}], which is in [{places.Count}] playlist place(s).",
this, GPALObjectType.YouTube);
1331 retVal = UploadAsReplacement(newVideo, found[0]);
1334 retVal = FinishReplacement(found[0], retVal, places);
1337 catch (Exception ex)
1339 string stopped =
null == retVal
1340 ? $
"ReplaceVideo failed for [{oldId}]."
1341 : $
"ReplaceVideo stopped before it finished. The new video is [{retVal.VideoId}].";
1353 private string TokenFor(
string operation)
1355 string token =
null;
1357 if (_credentials ==
null)
1359 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"{operation}: credentials not set.",
this, GPALObjectType.YouTube);
1363 _credentials.FetchAccessToken(out token);
1365 if (
string.IsNullOrEmpty(token))
1367 GPAL.
PublishSimpleEvent(GPALEventType.ERROR, $
"{operation}: failed to fetch access token.",
this, GPALObjectType.YouTube);
1377 private bool CallApi(
string method,
string url,
string token,
string contentType,
byte[] body, out
string answer)
1381 var req = (HttpWebRequest)WebRequest.Create(url);
1382 req.Method = method;
1383 req.Headers[
"Authorization"] =
"Bearer " + token;
1387 req.ContentType = contentType;
1388 req.ContentLength = body.Length;
1390 using (var s = req.GetRequestStream())
1391 s.Write(body, 0, body.Length);
1396 using (var resp = (HttpWebResponse)req.GetResponse())
1397 using (var reader =
new StreamReader(resp.GetResponseStream()))
1398 answer = reader.ReadToEnd();
1402 catch (WebException wex) when (wex.Response is HttpWebResponse errResp)
1405 using (var reader =
new StreamReader(errResp.GetResponseStream()))
1406 answer = $
"{(int)errResp.StatusCode} {Regex.Replace(reader.ReadToEnd(), @"\s+
", " ").Trim()}";
1414 private bool ReadAllPages(
string token,
string firstPageUrl, Action<JsonElement> onItem, out
string refusal)
1417 string pageToken =
null;
1422 string url =
null == pageToken ? firstPageUrl : firstPageUrl +
"&pageToken=" + Uri.EscapeDataString(pageToken);
1425 ok = CallApi(
"GET", url, token,
null,
null, out
string answer);
1433 using (var doc = JsonDocument.Parse(answer))
1435 if (doc.RootElement.TryGetProperty(
"items", out var items))
1436 foreach (var item
in items.EnumerateArray())
1439 if (doc.RootElement.TryGetProperty(
"nextPageToken", out var next))
1440 pageToken = next.GetString();
1444 while (ok && !
string.IsNullOrEmpty(pageToken));
1450 private string UploadsPlaylistId(
string token)
1452 string uploadsId =
null;
1454 if (!CallApi(
"GET", MyChannelUrl, token,
null,
null, out
string answer))
1456 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Could not read the channel: [{answer}]",
this, GPALObjectType.YouTube);
1460 using (var doc = JsonDocument.Parse(answer))
1462 if (doc.RootElement.TryGetProperty(
"items", out var items)
1463 && items.GetArrayLength() > 0
1464 && items[0].TryGetProperty(
"contentDetails", out var details)
1465 && details.TryGetProperty(
"relatedPlaylists", out var related)
1466 && related.TryGetProperty(
"uploads", out var uploads))
1467 uploadsId = uploads.GetString();
1470 if (
string.IsNullOrEmpty(uploadsId))
1472 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
"These credentials have no YouTube channel.",
this, GPALObjectType.YouTube);
1480 private bool ReadMyPlaylists(
string token, List<YouTubePlaylistInfo> into, out
string refusal)
1482 return ReadAllPages(token, MyPlaylistsUrl, item =>
1484 string playlistId = item.TryGetProperty(
"id", out var idEl) ? idEl.GetString() :
null;
1485 string title =
string.Empty, description =
string.Empty;
1486 YouTubePrivacy? privacy =
null;
1489 if (item.TryGetProperty(
"snippet", out var snippet))
1491 if (snippet.TryGetProperty(
"title", out var t)) title = t.GetString();
1492 if (snippet.TryGetProperty(
"description", out var d)) description = d.GetString();
1494 if (item.TryGetProperty(
"status", out var status) && status.TryGetProperty(
"privacyStatus", out var p))
1495 privacy = PrivacyFrom(p.GetString());
1496 if (item.TryGetProperty(
"contentDetails", out var details) && details.TryGetProperty(
"itemCount", out var c))
1497 c.TryGetInt32(out itemCount);
1499 if (!
string.IsNullOrEmpty(playlistId))
1500 into.Add(
new YouTubePlaylistInfo(playlistId, title, description, privacy, itemCount));
1504 private bool ReadPlaylistItems(
string token,
string playlistId, List<YouTubePlaylistItem> into, out
string refusal)
1506 return ReadAllPages(token, PlaylistItemsListUrl + Uri.EscapeDataString(playlistId), item =>
1508 string itemId = item.TryGetProperty(
"id", out var idEl) ? idEl.GetString() : null;
1509 string videoId = null, title = string.Empty;
1512 if (item.TryGetProperty(
"snippet", out var snippet))
1514 if (snippet.TryGetProperty(
"title", out var t)) title = t.GetString();
1515 if (snippet.TryGetProperty(
"position", out var p)) p.TryGetInt32(out position);
1516 if (snippet.TryGetProperty(
"resourceId", out var r) && r.TryGetProperty(
"videoId", out var v))
1517 videoId = v.GetString();
1520 into.Add(
new YouTubePlaylistItem(itemId, playlistId, videoId, title, position));
1527 bool deleted = CallApi(
"DELETE", PlaylistItemDeleteUrl + Uri.EscapeDataString(item.PlaylistItemId), token,
null,
null, out
string answer);
1530 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
1531 $
"Could not remove [{item.VideoId}] from playlist [{item.PlaylistId}]: [{answer}]",
this, GPALObjectType.YouTube);
1537 private void UpdateOneVideo(
string token,
string videoId)
1539 if (!CallApi(
"GET", VideoInfoBaseUrl + Uri.EscapeDataString(videoId), token,
null,
null, out
string current))
1541 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"UpdateVideo: could not read video [{videoId}]: [{current}]",
this, GPALObjectType.YouTube);
1547 using (var doc = JsonDocument.Parse(current))
1549 if (doc.RootElement.TryGetProperty(
"items", out var items)
1550 && items.GetArrayLength() > 0
1551 && items[0].TryGetProperty(
"snippet", out _))
1552 body = VideoUpdateJson(items[0], videoId);
1557 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"UpdateVideo: video [{videoId}] was not found.",
this, GPALObjectType.YouTube);
1562 string url =
"https://www.googleapis.com/youtube/v3/videos?part=" + (_privacyGiven ?
"snippet,status" :
"snippet");
1564 if (CallApi(
"PUT", url, token,
"application/json", body, out
string answer))
1565 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Updated video [{videoId}].",
this, GPALObjectType.YouTube);
1567 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"Failed to update video [{videoId}]: [{answer}]",
this, GPALObjectType.YouTube);
1575 private byte[] VideoUpdateJson(JsonElement video,
string videoId)
1578 JsonElement snippet = video.GetProperty(
"snippet");
1580 using (var ms =
new MemoryStream())
1582 using (var w =
new Utf8JsonWriter(ms))
1584 w.WriteStartObject();
1585 w.WriteString(
"id", videoId);
1587 w.WriteStartObject(
"snippet");
1588 w.WriteString(
"title", _titleGiven ? _title : StringOf(snippet,
"title"));
1589 w.WriteString(
"description", _descriptionGiven ? _description : StringOf(snippet,
"description"));
1590 w.WriteString(
"categoryId", _categoryGiven ? ((
int)_category).ToString() : StringOf(snippet,
"categoryId"));
1592 w.WriteStartArray(
"tags");
1595 foreach (
string tag
in _tags)
1596 w.WriteStringValue(tag);
1598 else if (snippet.TryGetProperty(
"tags", out var tags))
1600 foreach (var tag
in tags.EnumerateArray())
1601 w.WriteStringValue(tag.GetString());
1605 foreach (
string name
in new[] {
"defaultLanguage",
"defaultAudioLanguage" })
1606 if (snippet.TryGetProperty(name, out var language))
1607 w.WriteString(name, language.GetString());
1612 w.WriteStartObject(
"status");
1613 w.WriteString(
"privacyStatus", PrivacyString());
1616 var keep =
new List<string> {
"embeddable",
"license",
"publicStatsViewable",
"selfDeclaredMadeForKids",
"containsSyntheticMedia" };
1617 if (YouTubePrivacy.Private == _privacy)
1618 keep.Add(
"publishAt");
1620 if (video.TryGetProperty(
"status", out var status))
1621 foreach (var field
in status.EnumerateObject())
1622 if (keep.Contains(field.Name))
1630 retVal = ms.ToArray();
1636 private static string StringOf(JsonElement element,
string name)
1638 string retVal =
string.Empty;
1640 if (element.TryGetProperty(name, out var value) && JsonValueKind.String == value.ValueKind)
1641 retVal = value.GetString();
1648 private bool SetThumbnailBytes(
string token,
string videoId,
byte[] image,
string contentType, GPALEventType refusedAs)
1650 bool set = CallApi(
"POST", ThumbnailSetUrl + Uri.EscapeDataString(videoId), token, contentType, image, out
string answer);
1653 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Thumbnail set on [{videoId}].",
this, GPALObjectType.YouTube);
1655 GPAL.PublishSimpleEvent(refusedAs, $
"Thumbnail not set on [{videoId}]: [{answer}]",
this, GPALObjectType.YouTube);
1662 private bool FindPlaylistPlaces(
string token,
string videoId, out List<YouTubePlaylistItem> places)
1664 var found =
new List<YouTubePlaylistItem>();
1665 var playlists =
new List<YouTubePlaylistInfo>();
1666 bool ok = ReadMyPlaylists(token, playlists, out
string refusal);
1669 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"ReplaceVideo: could not read the playlists: [{refusal}]",
this, GPALObjectType.YouTube);
1671 for (
int i = 0; ok && i < playlists.Count; i++)
1673 var items =
new List<YouTubePlaylistItem>();
1674 ok = ReadPlaylistItems(token, playlists[i].PlaylistId, items, out refusal);
1677 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $
"ReplaceVideo: could not read playlist [{playlists[i].PlaylistId}]: [{refusal}]",
this, GPALObjectType.YouTube);
1679 found.AddRange(items.FindAll(item => videoId == item.VideoId));
1682 found.Sort((a, b) => a.PlaylistId == b.PlaylistId ? a.Position.CompareTo(b.Position) :
string.CompareOrdinal(a.PlaylistId, b.PlaylistId));
1692 string title = _title;
1693 string description = _description;
1694 string[] tags = _tags;
1695 YouTubePrivacy privacy = _privacy;
1696 YouTubeCategory category = _category;
1697 string playlistId = _playlistId;
1700 _description = old.Description;
1702 _privacy = old.Privacy ?? YouTubePrivacy.Private;
1703 _category =
int.TryParse(old.CategoryId, out
int categoryId) ? (YouTubeCategory)categoryId : _category;
1709 _description = description;
1712 _category = category;
1713 _playlistId = playlistId;
1721 string status = WaitUntilProcessed(uploaded.VideoId);
1724 if (
"failed" == status ||
"rejected" == status ||
"deleted" == status)
1726 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
1727 $
"ReplaceVideo: the new video [{uploaded.VideoId}] ended [{status}]. The playlists still hold [{old.VideoId}].",
1728 this, GPALObjectType.YouTube);
1733 _credentials.FetchAccessToken(out
string token);
1735 CopyThumbnail(token, old, uploaded.VideoId);
1737 foreach (var place
in places)
1738 TakePlace(token, place, uploaded.VideoId);
1740 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Replaced [{old.VideoId}] with [{uploaded.Url}].",
this, GPALObjectType.YouTube);
1748 private string WaitUntilProcessed(
string videoId)
1750 string status =
null;
1752 bool waiting =
true;
1756 _credentials.FetchAccessToken(out
string token);
1757 string now = GetUploadStatus(token, videoId);
1760 GPAL.PublishSimpleEvent(GPALEventType.INFO, $
"Video [{videoId}]: [{now}]",
this, GPALObjectType.YouTube);
1763 unanswered =
"unknown" == status ? unanswered + 1 : 0;
1764 waiting =
"processed" != status &&
"failed" != status &&
"rejected" != status &&
"deleted" != status
1765 && MaxUnansweredStatusChecks > unanswered;
1768 Thread.Sleep(_pollIntervalSecs * 1000);
1771 if (MaxUnansweredStatusChecks <= unanswered)
1772 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
1773 $
"Stopped waiting for [{videoId}] to process. YouTube did not answer [{unanswered}] checks in a row.",
this, GPALObjectType.YouTube);
1780 private void CopyThumbnail(
string token,
YouTubeVideoInfo old,
string newVideoId)
1782 if (
string.IsNullOrEmpty(old.ThumbnailUrl))
1784 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $
"ReplaceVideo: [{old.VideoId}] has no thumbnail to copy.",
this, GPALObjectType.YouTube);
1790 var req = (HttpWebRequest)WebRequest.Create(old.ThumbnailUrl);
1796 using (var resp = (HttpWebResponse)req.GetResponse())
1797 using (var s = resp.GetResponseStream())
1798 using (var ms =
new MemoryStream())
1801 image = ms.ToArray();
1802 contentType =
string.IsNullOrEmpty(resp.ContentType) ?
"image/jpeg" : resp.ContentType;
1805 SetThumbnailBytes(token, newVideoId, image, contentType, GPALEventType.WARNING);
1807 catch (Exception ex)
1809 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION,
1810 $
"ReplaceVideo: could not copy the thumbnail of [{old.VideoId}] to [{newVideoId}].",
this, GPALObjectType.YouTube, ex);
1819 bool added = AddVideoToPlaylist(token, place.PlaylistId, newVideoId, place.Position, GPALEventType.WARNING);
1821 if (!added && place.Position >= 0)
1823 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
1824 $
"Playlist [{place.PlaylistId}] would not take position [{place.Position}]. Adding [{newVideoId}] without one.",
this, GPALObjectType.YouTube);
1825 added = AddVideoToPlaylist(token, place.PlaylistId, newVideoId, -1, GPALEventType.ERROR);
1829 DeletePlaylistItem(token, place);
1832 private static string BestThumbnail(JsonElement snippet)
1834 string retVal =
null;
1836 if (snippet.TryGetProperty(
"thumbnails", out var thumbnails))
1837 foreach (
string size
in new[] {
"maxres",
"standard",
"high",
"medium",
"default" })
1838 if (
null == retVal && thumbnails.TryGetProperty(size, out var thumbnail) && thumbnail.TryGetProperty(
"url", out var url))
1839 retVal = url.GetString();
1844 private static YouTubePrivacy? PrivacyFrom(
string privacyStatus)
1846 YouTubePrivacy? retVal =
null;
1848 switch (privacyStatus)
1850 case "public": retVal = YouTubePrivacy.Public;
break;
1851 case "unlisted": retVal = YouTubePrivacy.Unlisted;
break;
1852 case "private": retVal = YouTubePrivacy.Private;
break;