0

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);
                }
DataMe
  • 95
  • 3
  • 10
  • Is it possible that you get only the column headers because no data was selected for the report? – Mike Meinz Apr 06 '17 at 17:05
  • I'm not sure what you mean, Mike. Please elaborate. The Reporting API has a predefined set of data which includes a 24 hour period of activity. We can only create a job asking for the report to be generated and then download the resulting csv. Am I misunderstanding? – DataMe Apr 06 '17 at 17:27
  • I am not an expert in the Reporting API. I read the following statement on [this page](https://developers.google.com/youtube/reporting/v1/reports/#step-6-download-the-report) of the documentation and wondered if that applied to your problem. "YouTube does generate downloadable reports for days on which no data was available. Those reports will contain a header row but will not contain additional data." – Mike Meinz Apr 06 '17 at 19:03
  • Oh right. Yes, I'm confident that there is data in YouTube. The user that allowed access to the YouTube scopes is the owner of the channel that I'm trying to access. There is a property in the api called "OnBehalfOfContentOwner". Definition = "The ID of the content owner for whom the API request is being made. If the request does not specify a value for this parameter, the API server assumes that the request is being made for the user's own channel." Frankly, I don't know how to get the Content Owner ID, but I'm researching this. It would be worth trying with this value set. – DataMe Apr 07 '17 at 15:47

1 Answers1

2

The final version of media.download in the post examples is correct.

I have a "Brand Account" in YouTube. This means that the user I sign into Google with has 2 "accounts". One account is associated with the Brand Account, and one is associated with my Google user.

I reset the Oath2 process by deleting the access & refresh token. This prompted me to again allow the app access to the requested scopes. First, I'm presented with a typical YouTube login screen. Then, I'm presented with a screen that ask me to select between two accounts; support@leapfrogbic.om or LeapFrogBI. The brand account is the one named LeapFrogBI.

It makes sense that the API would need to know which account to link to the application. It is quite confusing though. None of this is in any of the documentation I've found thus far. Frankly, the only reason I found this is because I started questioning whether I needed to pass in value for OnBehalfOfContentOwner. Although I'm still not 100% clear on the nomenclature, I think I understand what is going on.

With this information I went back and ran a couple tests. While Oath2 was set to use the LeapFrogBI account, I received no jobs! That was the big clue. So, I switched Oath2 to the support@leapfrogbi.com account and I got my expected job list. Of course, all of the videos are on the LeapFrogBI account so this would explain why I'm getting no data in the downloaded reports.

I created reports on the LeapFrogBI account, but it will take 2 days before the reports will be available. Fingers crossed. I'm hoping this problem is solved, but I'll definitely let you know what happens either way.

Confirmed. I'm getting data in the downloaded csv now.

DataMe
  • 95
  • 3
  • 10