I'm looking for an example which explains how to download reports using the .Net client library for YouTube Reporting API. I have authenticated, created jobs, and retrieved a list of reports including reportUrl values. I'm unable to figure out how to use the media.download method to actually download the report and save it locally.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Threading; //CancelationToken
using Google.Apis;
using Google.Apis.Auth.OAuth2;
using Google.Apis.YouTubeReporting.v1; //Scope of auth
using Google.Apis.YouTubeReporting.v1.Data;
using Google.Apis.Services;
using Google.Apis.Util.Store; //FileDataStore needs this
using System.Data.SqlClient;
namespace YouTubeReportingConsole
{
class Program
{
static void Main(string[] args)
{
//Console.WriteLine("Number of command line parameters = {0}", args.Length);
//for (int i = 0; i < args.Length; i++)
//{
// Console.WriteLine("Arg[{0}] = [{1}]", i, args[i]);
//}
if (args.Length > 0)
{
switch (args[0].ToLower())
{
case "reporttypeslist": //ReportTypesList returns a list of report types that are available
ListReportTypesResponse reportTypesListResponse = ytReportingService.ReportTypes.List().Execute();
foreach (ReportType reportType in reportTypesListResponse.ReportTypes)
{
WriteReportType(reportType, "Report Type List");
}
break;
case "jobcreate": //JobCreate creates jobs of the specified types. If the second argument equals "All", then a job is created for all availale report types.
for (int i = 1; i < args.Length; i++)
{
if (args[i].ToLower() == "all")
{ //Create a job for all available report types
ListReportTypesResponse reportTypesAvailableResponse = ytReportingService.ReportTypes.List().Execute();
foreach (ReportType reportType in reportTypesAvailableResponse.ReportTypes)
{
Job createdjob = CreateJob(reportType.Id);
WriteJob(createdjob, "Job Created");
}
break;
}
else
{ //Create job with id equal to the argument value
Job createdjob = CreateJob(args[i].ToLower());
WriteJob(createdjob, "Job Created");
}
}
break;
case "jobdelete":
for (int i = 1; i < args.Length; i++)
{
if (args[i].ToLower() == "all")
{ //Delete all jobs
ListJobsResponse jobDeleteList = ytReportingService.Jobs.List().Execute();
foreach (Job Job in jobDeleteList.Jobs)
{
ytReportingService.Jobs.Delete(Job.Id).Execute();
Console.WriteLine("Job Deleted: " + Job.Name);
}
break;
}
else
{ //Delete each job with id equal to the argument value
Job Job = ytReportingService.Jobs.Get(args[i]).Execute();
ytReportingService.Jobs.Delete(args[i]).Execute();
Console.WriteLine("Job Deleted: " + Job.Name);
}
}
break;
//case "getjob":
// break;
case "joblist":
ListJobsResponse jobListResponse = ytReportingService.Jobs.List().Execute();
if (jobListResponse.Jobs == null)
{
Console.WriteLine("No Jobs Returned");
}
else
{
foreach (Job Job in jobListResponse.Jobs)
{
WriteJob(Job, "Job List");
}
}
break;
//case "getreport":
// break;
case "reportlist":
//ReportList All [DataSource]
//ReportList [JobID] [DataSource]
//ReportList All [DataSource] [DownloadPath]
//ReportList [JobID] [DataSource] [DownloadPath]
SqlConnection conn = OpenConnection(args[2]);
VerifySchema(conn);
if (args[1].ToLower() == "all")
{ //List reports for all jobs
ListJobsResponse jobListResponseReport = ytReportingService.Jobs.List().Execute();
if (jobListResponseReport.Jobs == null)
{
Console.WriteLine("No Jobs Returned");
}
else
{
foreach (Job Job in jobListResponseReport.Jobs)
{
Report(Job.Id, conn, args[3]);
}
}
break;
}
else
{ //Single Job
Report(args[1], conn, args[3]);
}
break;
default:
break;
}
}
}
private static void Report(string JobId, SqlConnection conn, string path)
{
//Get the last created time. If empty then use 1900.
var reportList = ytReportingService.Jobs.Reports.List(JobId);
try
{
reportList.CreatedAfter = LastCreatedTime(conn, JobId);
}
catch
{
reportList.CreatedAfter = "1900-01-01T12:00:00.000000Z";
}
ListReportsResponse reportListResponse = reportList.Execute();
if (reportListResponse.Reports == null)
{
Console.WriteLine("No Reports for JobID: " + JobId);
}
else
{
foreach (Report report in reportListResponse.Reports)
{
//Download the report
var request = ytReportingService.Media.Download("");
//Insert report into history
string reportInsertSql = "INSERT INTO [dbo].[YouTubeReportHistory]" + "\n" +
"([reportId]" + "\n" +
",[jobId]" + "\n" +
",[startTime]" + "\n" +
",[endTime]" + "\n" +
",[createTime]" + "\n" +
",[jobExpireTime]" + "\n" +
",[downloadUrl])" + "\n" +
"VALUES" + "\n" +
"('" + report.Id + "'\n" +
",'" + JobId + "'\n" +
",'" + report.StartTime + "'\n" +
",'" + report.EndTime + "'\n" +
",'" + report.CreateTime + "'\n" +
",'" + report.JobExpireTime + "'\n" +
",'" + report.DownloadUrl + "')";
SqlCommand reportInsert = new SqlCommand(reportInsertSql, conn);
reportInsert.ExecuteNonQuery();
reportInsert.Dispose();
WriteReport(report, "Report");
}
}
}
private static string LastCreatedTime(SqlConnection conn, string JobId)
{
//*****************************************************************************************************************
//Library is not returning milliseconds. Adding 1 seconds to prevent collecting same report multiple times.
//Risk of missing report. Minimal chance that the same job will return more than one report in one second.
//*****************************************************************************************************************
SqlCommand comm = new SqlCommand("SELECT isnull(convert(varchar(50), dateadd(second, 1, max(cast([createTime] as datetimeoffset))), 127), '1900-01-01T12:00:00.000000Z') createTimeZulu FROM [dbo].[YouTubeReportHistory] where jobId = '" + JobId + "'", conn);
SqlDataReader reader = null;
reader = comm.ExecuteReader();
string LastCreatedTime = "";
if (reader.HasRows)
{
reader.Read();
LastCreatedTime = reader["createTimeZulu"].ToString();
reader.Close();
comm.Dispose();
}
else
{
LastCreatedTime = "1900-01-01T12:00:00.000000Z";
}
return LastCreatedTime;
}
//Feedback information only
private static void WriteReport(Report report, String Event)
{
// DateTime convertedDate = DateTime.SpecifyKind(DateTime.Parse(report.CreateTime.ToString()), DateTimeKind.Utc);
Console.WriteLine("================== Job Report ==================");
Console.WriteLine("Event: " + Event);
Console.WriteLine("ID: " + report.Id);
Console.WriteLine("DownloadUrl: " + report.DownloadUrl);
Console.WriteLine("CreateTime: " + report.CreateTime.ToString());
Console.WriteLine("StartTime: " + report.StartTime);
Console.WriteLine("EndTime: " + report.EndTime);
Console.WriteLine("JobID: " + report.JobId);
Console.WriteLine("JobExpireTime: " + report.JobExpireTime);
}
//Feedback information only
private static void WriteJob (Job job, String Event) {
Console.WriteLine("================== Reporting Job ==================");
Console.WriteLine("Event: " + Event);
Console.WriteLine("ID: " + job.Id);
Console.WriteLine("Name: " + job.Name);
Console.WriteLine("ReportTypeID: " + job.ReportTypeId);
Console.WriteLine("CreateTime: " + job.CreateTime);
Console.WriteLine("ExpireTime: " + job.ExpireTime);
}
//Feedback information only
private static void WriteReportType(ReportType reportType, String Event)
{
Console.WriteLine("================== Report Type ==================");
Console.WriteLine("Event: " + Event);
Console.WriteLine("ID: " + reportType.Id);
Console.WriteLine("Name: " + reportType.Name);
Console.WriteLine("SystemManaged: " + reportType.SystemManaged);
}
//Create a job
private static Job CreateJob(String ReportType)
{
Job job = new Job();
job.ReportTypeId = ReportType;
job.Name = ReportType; //Use the type as the name
Job createdjob = ytReportingService.Jobs.Create(job).Execute();
return createdjob;
}
private static String DeleteJob(String JobID)
{
ytReportingService.Jobs.Delete(JobID);
return JobID;
}
//Establish connection
private static SqlConnection OpenConnection(string connectionString)
{
//Data Source = WIN-IDD5KG7V0RT\SS2016;Integrated Security=true; Initial Catalog=Control; MultipleActiveResultSets=True;
//Database must exists
//Must have MARS = True
SqlConnection conn = new SqlConnection(connectionString);
try
{
conn.Open();
}
catch(Exception e)
{
Console.WriteLine(e.ToString());
}
return conn;
}
//Close connection
private static void CloseConnection(SqlConnection conn)
{
try
{
conn.Close();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
//Ensure schema is in place
private static void VerifySchema(SqlConnection conn)
{
//Ensure table exists
//Ensure db exists
String tblExists = "if not exists (Select * From sys.tables where name = 'YouTubeReportHistory')" + "\n" +
"Begin" + "\n" +
"CREATE TABLE[dbo].[YouTubeReportHistory](" + "\n" +
"[id][int] IDENTITY(1, 1) NOT NULL," + "\n" +
"[loadTime] [datetime] NOT NULL," + "\n" +
"[reportId] [varchar](50) NOT NULL," + "\n" +
"[jobId] [varchar](50) NOT NULL," + "\n" +
"[startTime] [datetime] NOT NULL," + "\n" +
"[endTime] [datetime] NOT NULL," + "\n" +
"[createTime] [datetime] NOT NULL," + "\n" +
"[jobExpireTime] [datetime] NOT NULL," + "\n" +
"[downloadUrl] [varchar](1000) NOT NULL," + "\n" +
"CONSTRAINT[PK_YouTubeReportHistory] PRIMARY KEY CLUSTERED([id] ASC)" + "\n" +
")" + "\n" +
"ALTER TABLE[dbo].[YouTubeReportHistory] ADD CONSTRAINT[DF_YouTubeReportHistory_loadTime] DEFAULT(sysdatetime()) FOR[loadTime]" + "\n" +
"End";
SqlCommand comm = new SqlCommand(tblExists, conn);
comm.ExecuteNonQuery();
comm.Dispose();
}
private static YouTubeReportingService ytReportingService = Auth();
private static YouTubeReportingService Auth()
{
UserCredential creds;
using(var stream = new FileStream("client_secret.json", FileMode.Open, FileAccess.Read))
{
creds = GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.Load(stream).Secrets,
new[] { YouTubeReportingService.Scope.YtAnalyticsMonetaryReadonly, YouTubeReportingService.Scope.YtAnalyticsReadonly },
"user",
CancellationToken.None,
new FileDataStore("YouTubeAPI")
).Result;
}
var service = new YouTubeReportingService(new BaseClientService.Initializer()
{
HttpClientInitializer = creds,
ApplicationName = "LeapFrogBIDataCollector"
});
return service;
}
}
}
Update 20170405
I believe the problem I'm seeing has something to do with the way the download URL is being provided. After a lot of head scratching and reviewing what little documentation I could find, I ended up with the below code which seems like an improvement, but this still only returns headers.
MediaResource.DownloadRequest getRequest = ytReportingService.Media.Download("");
using (var fileStream = new System.IO.FileStream(@filePath + reportTypeId + "_" + report.Id + ".csv", System.IO.FileMode.Create, System.IO.FileAccess.Write))
{
// Add a handler which will be notified on progress changes.
// It will notify on each chunk download and when the download is completed or failed.
getRequest.MediaDownloader.ProgressChanged += Download_ProgressChanged;
getRequest.MediaDownloader.Download(report.DownloadUrl, fileStream);
}
20170406 Update I found some examples of code in other languages which led me to making the following changes. At least the download URL is being passed in directly instead of me parsing it and using it as the resourcename. Sadly I'm getting the same results; headers only. No data.
//Download the report
MediaResource.DownloadRequest getRequest = ytReportingService.Media.Download("");
using (var fileStream = new System.IO.FileStream(@filePath + reportTypeId + "_" + report.Id + ".csv", System.IO.FileMode.Create, System.IO.FileAccess.Write))
{
// Add a handler which will be notified on progress changes. It will notify on each chunk download and when the download is completed or failed.
getRequest.MediaDownloader.ProgressChanged += Download_ProgressChanged;
getRequest.MediaDownloader.Download(report.DownloadUrl, fileStream);
}