【Windows】現在再生しているiTunes楽曲情報をX(Twitter)にポストします

以前まで[こちら](https://github.com/tumugin/NowPlayingV2/releases "Releases · tumugin/NowPlayingV2")のアプリを使ってTwitterにPCで再生している楽曲を投稿していたが、2023年6月頃から投稿ができなくなってしまいました。
なので~~私はこのアプリを勝手に改造して投稿ができるようにしようとしましたが~~、どうやらCoreTweet自体が現TwitterのAPIバージョンに対応していないという事実を知り、どうしようもなく路頭に迷ってしまいました。
そんな事はどうでもいいから投稿だけでもしたいと思い、味気のないバッチ的exeファイルを作成する事にしました。

# 使用するライブラリ
CoreTweetは無理でした。
**Tweetinviを使います。**

https://www.nuget.org/packages/TweetinviAPI/

:::note info
しかしTweetinviを使ってもTwitter API側に一部問題があり、私はそれを解決するために複数のAPIバージョンのポストを行っています。
これについてはよりスマートな解決策がある場合、それをコメントで書き込んでください。
あなたのその優しさを私は待っています😇
:::

iTunesは以下のアプリケーションの事です。

https://support.apple.com/ja-jp/HT210384

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/509804/d09fdbd4-a55a-ca77-e408-f07a2101b567.png)

iTunesの楽曲を取得するCOMライブラリについては後述。


# C# (.NET 7.0)
Windowsでしか動作確認してません。なぜなら私はMacintoshの保有者ではないからです。申し訳ありません。
Visual Studioで[C#] → [コンソールアプリ]でプロジェクトを作成し、[.NET 7.0] を選択しました。

Twitter APIのCONSUMER_KEYなどは取得しておいてください。
これは他の記事が役に立ちます。例えば以下の記事などです。

https://zenn.dev/nyancat/articles/20230803-twitter-api-oauth2-pkce


# code
``` C#:Program.cs
using System.Text;
using iTunesLib;                // iTunes COM object models
using Newtonsoft.Json;
using Tweetinvi;                // Twitter API wrapper library.
using Tweetinvi.Core.Web;
using Tweetinvi.Models;


// Tweetinvi -> Get the https://www.nuget.org/profiles/tweetinvi
// (NuGet Package) PM> "Install-Package TweetinviAPI"

namespace iTunesNowPlayingTweet
{
    class Program
    {
        static async Task Main(string[] args)
        {
            // Activation of Twitter API V2 is mandatory.
            // Please input any value of your choice individually.
            const string CONSUMER_KEY    = "******************************";
            const string CONSUMER_SECRET = "******************************";
            const string ACCESS_KEY      = "******************************";
            const string ACCESS_SECRET   = "******************************";

            // Initialization of the iTunes COM object model and retrieval of the currently playing track.
            iTunesApp iTunes = new();
            IITTrack currentTrack = iTunes.CurrentTrack;

            // Create the text of the tweet.
            string tweetText = $"{currentTrack.Name} - {currentTrack.Artist} \r\nsongs from [{currentTrack.Album}]\r\n#NowPlaying";

            // Set Twitter API key and secret key.
            var userCredentials = new TwitterClient(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_KEY, ACCESS_SECRET);
            var userClient = new TweetsV2Poster(userCredentials);

            // Get the album artwork.
            IITArtworkCollection artworks = currentTrack.Artwork;
            if (artworks != null && artworks.Count > 0)
            {
                // If you are attaching artwork, we are using the V1.1a endpoint because the endpoint for Twitter API V2 is unknown.
                // CAUTION : There is a high likelihood that the mechanism will change in the future.
                IITArtwork artwork = artworks[1];
                string tempImagePath = "D:/album_artwork.jpg";      // WARNING : Reconfigure to a safe directories!
                artwork.SaveArtworkToFile(tempImagePath);
                var media = await userCredentials.Upload.UploadBinaryAsync(File.ReadAllBytes(tempImagePath));
                File.Delete(tempImagePath);

                // Go post.
                ITwitterResult result = await userClient.PostTweet(new TweetV2PostRequest
                {
                    Text = tweetText,
                    Media = media?.Id == null ? null : new(){ MediaIds = new() { media.Id.Value } }
                });
                Console.WriteLine("ツイートが投稿されました: " + tweetText);
            }
            else
            {
                // HACK : If there is no artwork, I will post plain text only.
                // It should work with the following code. However, I haven't verified it as it was not needed. Sorry.
                // Go post.
                //ITwitterResult result = await userClient.PostTweet(new TweetV2PostRequest{ Text = tweetText });
                //Console.WriteLine("ツイートが投稿されました: " + tweetText);
            }
        }
    }


    /// <summary>
    ///  The source is https://github.com/linvi/tweetinvi/issues/1147#issuecomment-1173174302
    /// </summary>
    public class TweetsV2Poster
    {
        // ----------------- Fields ----------------

        private readonly ITwitterClient client;

        // ----------------- Constructor ----------------

        public TweetsV2Poster(ITwitterClient client)
        {
            this.client = client;
        }

        public Task<ITwitterResult> PostTweet(TweetV2PostRequest tweetParams)
        {
            return this.client.Execute.AdvanceRequestAsync(
                (ITwitterRequest request) =>
                {
                    var jsonBody = this.client.Json.Serialize(tweetParams);

                    // Technically this implements IDisposable,
                    // but if we wrap this in a using statement,
                    // we get ObjectDisposedExceptions,
                    // even if we create this in the scope of PostTweet.
                    //
                    // However, it *looks* like this is fine.  It looks
                    // like Microsoft's HTTP stuff will call
                    // dispose on requests for us (responses may be another story).
                    // See also: https://stackoverflow.com/questions/69029065/does-stringcontent-get-disposed-with-httpresponsemessage
                    var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");

                    request.Query.Url = "https://api.twitter.com/2/tweets";
                    request.Query.HttpMethod = Tweetinvi.Models.HttpMethod.POST;
                    request.Query.HttpContent = content;
                }
            );
        }
    }


    /// <summary>
    /// There are a lot more fields according to:
    /// https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/api-reference/post-tweets
    /// but these are the ones we care about for our use case.
    /// </summary>
    public class TweetV2PostRequest
    {
        /// <summary>
        /// The text of the tweet to post.
        /// </summary>
        [JsonProperty("text")]
        public string Text { get; set; } = string.Empty;

        [JsonProperty("media", NullValueHandling = NullValueHandling.Ignore)]
        public TweetV2Media? Media { get; set; }
    }


    /// <summary>
    /// It seems that tweetinvi does not currently have support for posting tweets with associated media using the Twitter V2 API.
    /// https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/api-reference/post-tweets
    /// https://stackoverflow.com/questions/76297300/c-sharp-tweet-api-v2-attach-media-to-tweet-create-jsonproperty
    /// </summary>
    public class TweetV2Media
    {
        [JsonIgnore]
        public List<long>? MediaIds { get; set; }

        [JsonProperty("media_ids", NullValueHandling = NullValueHandling.Ignore)]
        public string[]? MediaIdStrings
        {
            get => MediaIds?.Select(i => JsonConvert.ToString(i)).ToArray();
            set => MediaIds = value?.Select(s => JsonConvert.DeserializeObject<long>(s)).ToList();
        }

        // Add others here as required, setting NullValueHandling.Ignore or DefaultValueHandling = DefaultValueHandling.Ignore to suppress unneeded properties
    }
}

```

iTunesのCOMライブラリは以下の参照を追加してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/509804/44121af6-804b-872f-b2cd-810eb0ef514d.png)

あと以下の部分は適宜書き換えてからご使用ください。
``` C#:Program.cs
string tempImagePath = "D:/album_artwork.jpg";      // WARNING : Reconfigure to a safe directories!
```

基本的に私はコードを書いていません。
なので詳しい説明はできません。
ポスト内容を変更したい方は自己責任で改変して使ってもらって結構です。
しかしこのコードはいつTwitter側の仕様変更で使えなくなるか分かりませんし、それについては私もよく知りません。
なので完全に自己判断で利用してください。(できればどなたか派生した内容の詳細な説明をした記事を書いてほしいです)
コードを書いたのは、以下の有志の方々です。

* Seth Hendrick 氏 (https://github.com/xforever1313)
* dbc 氏 (https://stackoverflow.com/users/3744182/dbc)
* Hisaya 氏 (https://hsy.me/)
* ChatGPT-3.5 氏 (https://chat.openai.com/)

また参考にしたサイト、引用元についてはコード内のコメントを参照ください。


# 実行結果
成功した場合は以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/509804/78cc5ea9-3f88-cb20-2fcc-6caf071ca97f.png)



# その他備考
何かあれば追記します。