ラジオの録音とCopilotの話

ラジオ番組の録音設定を簡単にできないか、とCopilotを使って見た、2つの話です。

これまでの経緯をまとめると
もうひとつのアプリ でカレンダー形式のDLNAプレーヤ
Androidアプリ再び でその実装を考えていました。
これは大量の録音を俯瞰できるようにするためでしたが、代わりにその日ごとのm3uプレイリストの自動生成をすることで目的をほぼ達成できているので、もう必要ないかなと考えています。

少し前から、ラジオ番組表で番組をタップして簡単に録音のset/unsetできるようにできないか考えていました。2週間くらい前に具体的に設計したのが次の図です。

Googleカレンダーを経由して各局のラジオ番組表からラジオ録音表へコピーして、録音をsetする。ラジオ録音表から削除でunsetする。という流れです。

ただ、いわゆる「繰り返し」の部分で手作業が必要になるんですね。基本すべての番組を「毎週」としてしまえばいい。けれど、帯番組で月曜から木曜あるものは月曜分だけコピーして、それを「月〜木の繰り返し」と手で設定する、とか必要になる。超A&G+のリピート放送は2番組扱いでいいか。

仕事でcopilotを使うことになったので、タブレットにインストールしていたCopilotアプリを試しに動かしてみました。上の設計を実装することを試してみます。

「C#で、google calenderのICSをcrontabに変換するツールを作って」のような文章を入力すると、次のようなコードを出してくれました。

using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using Ical.Net;
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes;

namespace IcsToCrontab
{
    class Program
    {
        static void Main(string[] args)
        {
            // GoogleカレンダーのICSファイルのURLを指定
            string url = "https://calendar.google.com/calendar/ical/.../basic.ics";
            // ICSファイルをダウンロードして文字列として読み込む
            string ics = DownloadIcs(url);
            // ICSファイルをカレンダーオブジェクトに変換する
            Calendar calendar = Calendar.Load(ics);
            // カレンダーのイベントを取得する
            var events = calendar.Events;
            // イベントごとにcrontabの形式に変換する
            foreach (var e in events)
            {
                // イベントの開始日時と終了日時を取得する
                var start = e.Start.AsDateTimeOffset;
                var end = e.End.AsDateTimeOffset;
                // イベントのタイトルを取得する
                var title = e.Summary;
                // crontabの形式に変換する
                // 分 時 日 月 曜日 コマンド
                // コマンドはechoでタイトルを出力するものとする
                var crontab = $"{start.Minute} {start.Hour} {start.Day} {start.Month} * echo \"{title}\"";
                // crontabを出力する
                Console.WriteLine(crontab);
            }
        }

        // ICSファイルをダウンロードして文字列として読み込むメソッド
        static string DownloadIcs(string url)
        {
            // WebClientを作成する
            using (var client = new WebClient())
            {
                // 文字コードをUTF-8に指定する
                client.Encoding = Encoding.UTF8;
                // ICSファイルをダウンロードして文字列として返す
                return client.DownloadString(url);
            }
        }
    }
}

なるほど、c#にはIcalクラスがあってパースしてくれるのね。google calenderからダイレクトにIcal受け取れるのかな?とかAPIも調査する手間が省けました。

次に「c#で、radikoの番組表を取得してgoogle calenderに表示するコードを作って」とすると、

using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Xml;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Calendar.v3;
using Google.Apis.Calendar.v3.Data;
using Google.Apis.Services;

namespace RadikoToCalender
{
    class Program
    {
        // radikoのAPIのURL
        private const string RadikoApiUrl = "http://radiko.jp/v3/program/station/weekly/{0}.xml";

        // google calenderのAPIのスコープ
        private static readonly string[] Scopes = { CalendarService.Scope.Calendar };

        // google calenderのAPIのクライアントID
        private const string ClientId = "YOUR_CLIENT_ID";

        // google calenderのAPIのクライアントシークレット
        private const string ClientSecret = "YOUR_CLIENT_SECRET";

        // google calenderのID
        private const string CalendarId = "YOUR_CALENDAR_ID";

        static async Task Main(string[] args)
        {
            // radikoの放送局のID
            string stationId = "TBS";

            // radikoの番組情報を取得する
            var programs = await GetProgramsFromRadikoAsync(stationId);

            // google calenderのサービスを作成する
            var service = CreateCalendarService();

            // google calenderに予定を追加する
            await AddEventsToCalendarAsync(service, programs);
        }

        // radikoの番組情報を取得するメソッド
        private static async Task<JArray> GetProgramsFromRadikoAsync(string stationId)
        {
            // HttpClientを作成する
            using var client = new HttpClient();

            // radikoのAPIのURLを作成する
            string url = string.Format(RadikoApiUrl, stationId);

            // radikoのAPIにリクエストを送信し、レスポンスを受け取る
            var response = await client.GetAsync(url);

            // レスポンスが成功でなければ例外をスローする
            response.EnsureSuccessStatusCode();

            // レスポンスの内容を文字列として読み込む
            var xml = await response.Content.ReadAsStringAsync();

            // XmlDocumentを作成し、XMLを読み込む
            var doc = new XmlDocument();
            doc.LoadXml(xml);

            // 番組情報を格納するJArrayを作成する
            var programs = new JArray();

            // XMLから番組情報を取り出す
            foreach (XmlNode program in doc.SelectNodes("//prog"))
            {
                // 番組の開始時間と終了時間を取得する
                string start = program.Attributes["ft"].Value;
                string end = program.Attributes["to"].Value;

                // 番組のタイトルと説明を取得する
                string title = program.SelectSingleNode("title").InnerText;
                string description = program.SelectSingleNode("desc").InnerText;

                // 番組情報をJObjectとして作成する
                var obj = new JObject();
                obj["start"] = start;
                obj["end"] = end;
                obj["title"] = title;
                obj["description"] = description;

                // 番組情報をJArrayに追加する
                programs.Add(obj);
            }

            // 番組情報のJArrayを返す
            return programs;
        }

        // google calenderのサービスを作成するメソッド
        private static CalendarService CreateCalendarService()
        {
            // クライアントIDとクライアントシークレットを用いてクライアントシークレットを作成する
            var clientSecrets = new ClientSecrets();
            clientSecrets.ClientId = ClientId;
            clientSecrets.ClientSecret = ClientSecret;

            // ユーザーの認証を行う
            var credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
                clientSecrets,
                Scopes,
                "user",
                CancellationToken.None).Result;

            // google calenderのサービスを作成する
            var service = new CalendarService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = "RadikoToCalender",
            });

            // google calenderのサービスを返す
            return service;
        }

        // google calenderに予定を追加するメソッド
        private static async Task AddEventsToCalendarAsync(CalendarService service, JArray programs)
        {
            // 番組情報のJArrayから予定を作成する
            foreach (JObject program in programs)
            {
                // 予定の開始時間と終了時間を取得する
                string start = program["start"].ToString();
                string end = program["end"].ToString();

                // 予定のタイトルと説明を取得する
                string title = program["title"].ToString();
                string description = program["description"].ToString();

                // 予定をEventとして作成する
                var event = new Event();
                event.Summary = title;
                event.Description = description;
                event.Start = new EventDateTime();
                event.Start.DateTime = DateTime.ParseExact(start, "yyyyMMddHHmmss", null);
                event.End = new EventDateTime();
                event.End.DateTime = DateTime.ParseExact(end, "yyyyMMddHHmmss", null);

                // google calenderに予定を追加する
                await service.Events.Insert(event, CalendarId).ExecuteAsync();
            }
        }
    }
}

radikoの番組表取得のAPIからgoogle calenderへの入力まで教えてくれました。

どちらも実際に動かしていないのですが、アウトラインは思ったより簡単で、工数は考えていたより少なそうです。

Copilotを触ってみた感触としては「使用するAPIの調査が楽になる」が第一印象です。もちろんAPIの検証が必要ですが。今回は全体の設計は可能であることを確認しているので、その道筋が短縮できた所が大きなポイントでした。

同じように設計をCopilotに聞いてみるとどうなのか、ちょっと未知数です。上の2番目の例では、初め質問の仕方が悪くてコードではなく手順を文章で説明してくれました。つまり、簡単な(具体的な)設計ですね。

Copilotの提案した設計の評価は難しいかも。コードは動かして確認することができますが、幾つかの設計を出して比較・検討するためにはうまく質問しないといけない。実装に入ってから設計のミスに気づく、なんてことも大いにありそうです。それはCopilot使わなくても同じですが…

「生成AIをうまく使うスキルは、人を上手に使うスキルに似ている」とは言われるようですが、これも経験が必要な技能ですよね、技術ではなくて。

さて、「ラジオ番組の録音を簡単にset/unsetする」ですが、今の所は技能的に番組表を見て手でcrontabに書き込む方が楽そうです。アプリを作って技術にするメリットがあまりない…