macOSのメニューバーにお気に入りのニュースを表示

## 背景

あまりニュースを見ない（新聞もテレビも）という悪いクセがあるので、MacBook Proのメニューバーに、ニュースのヘッドラインを表示できるようにしてみました。

- ニュース提供元ごとに記事をまとめます
- 記事が多い場合、15記事のみ表示します
- 記事をクリックすると、ウェブサイトにジャンプします
- 記事タイトルの頭に「日付-時間:」を表示します

という仕様にしています。

![SS 2022-01-02 8.20.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184128/fec4f3e1-4ef7-2981-8163-106082dc03b4.png)

### 環境

ここで書いていることは、下記のバージョンで実施しました。

- feedparser 6.0.8
- SwiftBar 1.4.2 (386)
- VS Code 1.63.2
- macOS Monterey 12.1
- MacBook Pro (13-inch, 2020, Four Thunderbolt 3 Ports)

## 手順の概要

1. SwiftBarのインストール（メニューバーカスタマイズツール）
1. Pythonのインストール
1. feedparser（Pythonモジュール）のインストール
1. Pythonスクリプトの記述

SwiftBarというメニューバーカスタマイズツールがあり、これに対して標準出力を吐けばメニューバーに表示してくれます。RSSフィードのパーサとして`feedparser`がわりとよさそうだったので、つかうことにしました。Pythonスクリプトを書くのははじめてなので、あまり流儀に沿っていないところもあるかもしれません。ご容赦ください。

## 手順の詳細

### 1. SwiftBarのインストール

[Rebuild.fm](https://rebuild.fm)の[第292回](https://rebuild.fm/292/)で、[SwiftBar](https://github.com/swiftbar)というツールが紹介されていました。

macOSのメニューバーのカスタマイズツールで、スクリプトの標準出力をメニューバーに出してくれます。

[SwiftBarのリリースページ](https://github.com/swiftbar/SwiftBar/releases)から最新版をzipダウンロードし、任意のフォルダ（ふつうはアプリケーションフォルダ？）に置き、起動します。プラグインフォルダを指定するように言われるので、適当なフォルダを指定します。

> あまり深い階層にプラグインフォルダをおくと「ダメ」と言われるっぽいので、ユーザのホーム（`~/`）下にフォルダをおきました。

#### 補足

BitBarというわりと有名なアプリがあったようです。これが新しいmacOSで使えなくなって、後継版として[xBar](https://xbarapp.com)と[SwiftBar](https://github.com/swiftbar)の2つに進化したようです。いずれもBitBar時代のプラグイン資産を使えます。xBarのほうがGitHubのスターが多くて人気があるようです。しかしながら、

- わたしはうまくxBarを動かせなかった（原因は不明）
- SwitBarは`SF Symbols`がつかえる

ので、SwiftBarをつかっています。

### 2. Pythonのインストール

以前に[macOSでpythonを使えるようにした](https://qiita.com/hann-solo/items/674140a69bcb48aba38c)はずなんですけれども、python3を使おうとするとまたクラッシュするようになっていました。Montereyにアップデートしたからですかねえ（謎）。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184128/a6f5bf3f-b11b-d25b-8bf3-6cfbe24790d2.png" width=40%>

```bash
$ python3 --version
dyld[1993]: dyld cache '/System/Library/dyld/dyld_shared_cache_x86_64h' not loaded: syscall to map cache into shared region failed
dyld[1993]: Library not loaded: /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
  Referenced from: /Library/Frameworks/Python.framework/Versions/3.6/Resources/Python.app/Contents/MacOS/Python
  Reason: tried: '/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation' (no such file), '/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation' (no such file)
Abort trap: 6

$ python3 -m pip install requests
dyld[6053]: dyld cache '/System/Library/dyld/dyld_shared_cache_x86_64h' not loaded: syscall to map cache into shared region failed
dyld[6053]: Library not loaded: /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
  Referenced from: /Library/Frameworks/Python.framework/Versions/3.6/Resources/Python.app/Contents/MacOS/Python
  Reason: tried: '/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation' (no such file), '/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation' (no such file)
Abort trap: 6
```

しかたがないので、再度インストール。

```
$ brew install python3
Error: python@3.9: the bottle needs the Apple Command Line Tools to be installed.
  You can install them, if desired, with:
    xcode-select --install

You can try to install from source with:
  brew install --build-from-source python@3.9
Please note building from source is unsupported. You will encounter build
failures with some formulae. If you experience any issues please create pull
requests instead of asking for help on Homebrew's GitHub, Twitter or any other
official channels.
```
と言われたので（入れてあるはずなんだけどなあとおもいながら）

```
$ xcode-select --install
xcode-select: note: install requested for command line developer tools
```

そしてふたたび

```
$brew install python3
```

無事に完了しました。

#### 補足

macOSでのPythonについては、[macOS環境のPython](https://www.python.jp/install/macos/index.html)をお手本にしました。

ほんとうは`venv`などの仮想環境下でモジュールをインストールしたほうがいいのですけれども、今回はSwiftBarのためにPythonスクリプトを動かしたいので、仮想環境をつかいませんでした。

SwiftBarから動かすスクリプトを、仮想環境で動かしたほうがいいんだろうなとはおもっています。

### 3. feedparserのインストール

ニュースサイトのRSSフィードを解釈するツールとして、[feedparser](https://github.com/kurtmckee/feedparser)を選びました。

```
$ python3.9 -m pip install feedparser
```

#### 補足

GitHubで`rss parser`を調べて、[rss-parser](https://github.com/dhvcc/rss-parser)も使おうとしてみましたが、なぜだかうまく動かなかったので、最初にうまく動いたfeedparserに。

つかいかたの入門には、[Using Feedparser in Python](https://www.pythonforbeginners.com/feedparser/using-feedparser-in-python)がとても参考になりました。

だいたいわかってきてからは、[feedparserの公式ドキュメント](https://feedparser.readthedocs.io/en/latest/)が使いやすかったです。

### 4. Pythonスクリプトの記述

いまのところ下記のスクリプトで落ち着いています。

- ニュース提供元ごとに記事をまとめます
- 記事が多い場合、15記事のみ表示します
- 記事をクリックすると、ウェブサイトにジャンプします
- 記事タイトルの頭に「日付-時間:」を表示します

ファイル名を`feedparser.60m.py`として、SwiftBarのプラグインフォルダに置きます。

`60m`は、60分ごとに実行する、という意味です。

```Python
#!/usr/bin/env python3.9
# -*- coding: utf-8 -*-
#
# sources - source - parsed(d) + feed 
#                              + entries[i]

import feedparser

MAX_ENTRIES  = 15
ENTRY_LENGTH = 30
SUBMENU    = '--'
SUBSUBMENU = '----'
IDX_DAY  = 2
IDX_HOUR = 3
DAY_DELIM  = '-'
HOUR_DELIM = ':'
MISS_MONTH = 'xx'
MISS_HOURS = 'xx'

sources = [
    # {'cite':'Apple',    'url':'https://www.apple.com/jp/newsroom/rss-feed.rss'},
    {'cite':'京都新聞', 'url':'https://www.kyoto-np.co.jp/list/feed/rss'},
    {'cite':'NHK',      'url':'https://www.nhk.or.jp/rss/news/cat0.xml'},
    {'cite':'ハフポス', 'url':'https://www.huffingtonpost.jp/feeds/index.xml'}, 
    {'cite':'CNET',     'url':'http://feed.japan.cnet.com/rss/index.rdf'},
    {'cite':'ギズモ',   'url':'https://www.gizmodo.jp/index.xml'},
    {'cite':'ParsToday','url':'https://parstoday.com/ja/rss'},
    {'cite':'ロイター', 'url':'https://assets.wor.jp/rss/rdf/reuters/top.rdf'}
]

# generate parameter strings
entry_param = ' | length=' + str(ENTRY_LENGTH) + ' href='

# menubar header
print(':newspaper.fill:')
print('---')

# bodies
for source in sources:
    parsed = feedparser.parse(source['url'])

    # show source
    print (SUBMENU + parsed.feed.title)

    # debug: to see keys list of entry
    # print (parsed.entries[0].keys())

    # show entries
    no_display = min(len(parsed['entries']), MAX_ENTRIES)

    for i in range(no_display):
        e = parsed.entries[i]
        t = ' '.join(e.title.split())
        l = e.link

        # set date and hour
        if ('published_parsed' in e):
            d = '{:02}'.format(e.published_parsed[IDX_DAY])
            h = '{:02}'.format(e.published_parsed[IDX_HOUR])
        elif ('date' in e):
            # note: avoid date format hell...
            d = e.date[8:10]
            h = e.date[11:13]
        else:
            d = MISS_MONTH
            h = MISS_HOURS

        d = d + DAY_DELIM
        h = h + HOUR_DELIM

        print (SUBSUBMENU + d + h + t + entry_param + l)
```

スクリプトに対して、

```bash
$ chmod +x feedparser.60m.py

$ ll
-rwxr-xr-x@  1 masatora  staff  2369  1  1 17:35 feedparser.60m.py*
```

として、実行権限を与えます。


#### 補足1. 実行環境

VS Codeだと、▶️ボタンを押すと`Terminal`で実行してくれるんですね。これを発見してから、スクリプトを書くのがはかどりました。

#### 補足2. ハマりどころ

`feedparser`のドキュメントやイシューをみていると`d.entries[0].published_parsed`で日付情報が取れるハズなんですけれども、実際には取れないサイトがあります。

```
print (parsed.entries[0].keys())
```

で、どういうキーを取得しているのかサイトごとに調べておいて、

```
if ('published_parsed' in e):
    xxxx
elif ('date' in e):
    xxxx
```

などと、存在するキーを確かめてから日付情報を取得するようにしました。

なお、日付情報が格納されているフォーマットは、

- standard Python 9-tuple in UTC

> time.struct_time(tm_year=2021, tm_mon=12, tm_mday=2, tm_hour=8, tm_min=50, tm_sec=17, tm_wday=3, tm_yday=336, tm_isdst=0)

- ISO8601 format

> 2021-12-02T08:50:17.489Z
> 2022-01-01T08:51:00+09:00

の2種類でした（RSS提供元によっては他にもあるかも）。

とくにISO8601をきちんと日付オブジェクトに変換するのはハマりそうだったので深入りせず、文字列から日付と時刻だけを切り出すという簡易的な方法をとりました。

### おわりに

さて、これでニュースを多少は見るようになるか！？

元ネタは、iOSや（さいきんはmacOSでもつかえるようになった）ショートカットアプリの「トップニュースをブラウズ」です。これをカスタマイズして、iPhoneでは便利につかっていました。ニュースを見るかどうかはムラがあるんですけども。

ショートカットも改良して、iPhoneでのニュース環境も改善したいなあ。

そのためにはまず「どのニュースサイトなら見る気になるか」の自己観察をしばらく続けてみることからでしょうか。