2024年10月31日木曜日

最新の dev_appserver.py の使い方

最新の dev_appserver.py の使い方

概要

過去の手順だと動作しないランタイムがあるので最新の手順を紹介します

環境

  • macOS 15.0.1
  • Python 3.11.10
  • google-cloud-sdk 491.0.0
  • golang runtime 122

google-cloud-sdk のインストール

  • brew install google-cloud-sdk

dev_appserver.py があるか確認

  • ls /opt/homebrew/share/google-cloud-sdk/bin/dev_appserver.py

python の設定

グローバルにインストールした python3 でも OK です

  • pyenv local 3.11.10

実行

  • python /opt/homebrew/share/google-cloud-sdk/bin/dev_appserver.py .

最後に

すでにサポートされていないランタイムだとこの方法では動作しないので注意してください

参考サイト

2024年10月30日水曜日

Python の JIT numba を使う

Python の JIT numba を使う

概要

とりあえず試せるコードを紹介します

環境

  • macOS 15.0.1
  • Python 3.11.10
  • numba 0.60.0

インストール

  • pipenv install numpy numba

サンプルコード

import time

import numpy as np
from numba import jit


# 通常のPython関数
def sum_array(arr):
    total = 0
    for i in arr:
        total += i
    return total


# Numbaを使用した関数
@jit(nopython=True)
def sum_array_numba(arr):
    total = 0
    for i in arr:
        total += i
    return total


# 配列の準備
array_size = 10**7
arr = np.random.rand(array_size)

# 通常のPython関数の実行時間を計測
start_time = time.time()
sum_array(arr)
end_time = time.time()
print(f"通常のPython関数の実行時間: {end_time - start_time} 秒")

# Numbaを使用した関数の実行時間を計測
start_time = time.time()
sum_array_numba(arr)
end_time = time.time()
print(f"Numbaを使用した関数の実行時間: {end_time - start_time} 秒")

結果

  • pipenv run python app.py
通常のPython関数の実行時間: 0.46547412872314453 秒
Numbaを使用した関数の実行時間: 0.17465901374816895 

確かに速いです

最後に

使い所としては計算処理なので Web アプリなどでは使い所が難しいです
機械学習で使うにしてもライブラリ側ですでに使っているケースなどもあるので自分で使うケースがないのかも

2024年10月29日火曜日

(unattended-upgrade) Could not figure out development release: Distribution data outdated. Please check for an update for distro-info-data. See usr share doc distro-info-data README.Debian for details.

(unattended-upgrade) Could not figure out development release: Distribution data outdated. Please check for an update for distro-info-data. See usr share doc distro-info-data README.Debian for details.

概要

Ubuntu を 22 -> 24 に更新したら unattended-upgrade が動かくなったのでその対応です

環境

  • Ubuntu 24.04

対応方法

  • sudo apt install --only-upgrade distro-info-data

動作確認

  • sudo unattended-upgrade

最後に

distro-info-data が追加でインストールする必要がありました

参考サイト

2024年10月28日月曜日

Google Chrome をプロフィールを指定して open コマンドで開く方法

Google Chrome をプロフィールを指定して open コマンドで開く方法

概要

プロフィールはなぜかインデックス番号付きのディレクトリで管理されているのでそのディレクトリ名を指定することでプロフィール指定して Chrome を開くことができます

環境

  • macOS 15.0.1
  • Google Chrome 129.0.6668.100

コマンド

  • open -a "Google Chrome" --args --profile-directory="Profile 1"

プロフィールがどの Google アカウントに紐づくか確認する方法

なぞにディレクトリにスペースが含まれているので find + while を使って Profile ディレクトリ配下にある Preferences ファイルを調査する感じです

  • cd ~/Library/Application\ Support/Google/Chrome/
  • find . -type d -name "Profile\ *" | while IFS= read -r i; do echo ${i}; cat ${i}/Preferences | jq '.account_info[].full_name'; done

最後に

このプロフィールディレクトリのインデックス番号は追加したプロフィール順になるのでもし環境をまたいで同じプロフィール番号で起動する場合には追加する順番に注意が必要です

2024年10月26日土曜日

RaspberryPi5 に castsponsorskip をインストールする

RaspberryPi5 に castsponsorskip をインストールする

概要

CastSponsorSkip が MacOS 上だとなぜか動作しなかったので RaspberryPi5 上で動かしてみました

環境

  • RaspberryPi5
  • Raspbian (bookwarm aarch64)
  • docker 27.3.1
  • castsponsorskip 0.8.0
  • Chromecast with GoogleTV

docker のインストール

公式のDebian 64bit用のインストール手順をそのまま使います

for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get remove $pkg; done

sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

sudo が面倒な場合は docker グループに追加します

  • sudo gpasswd -a $USER docker

コマンド補完が必要な場合は以下も実行します

cat <<EOT >> ~/.bashrc
if [ -f /etc/bash_completion ]; then
    . /etc/bash_completion
fi
EOT

YoutubeAPI キーの取得

GCP のプロジェクトを作成し YouTube Data API v3 を有効にし認証情報から API キーを作成します 過去にも方法を紹介しているのでこの記事を参考にするのがいいかなと思います

CastSponsorSkip の起動

  • docker run --network=host --name=castsponsorskip -e CSS_YOUTUBE_API_KEY=AIzaxxxxxxx ghcr.io/gabe565/castsponsorskip

トラブルシューティング

quotaExceeded

以下のエラーが発生しました
よくわかりませんが再度 API キーを作成した直りました
キーが古すぎるとダメなのかもしれません

にも同じ現象にあっていてそのときはプロジェクト自体新しく作成したようです

2024-10-22 04:15:36 ERR YouTube search failed. device="ファミリー ルーム 2" error="googleapi: Error 403: The request cannot be completed because you have exceeded your <a href=\"/youtube/v3/getting-started#quota\">quota</a>., quotaExceeded"

ログ

正常動作時のログになります

2024-10-22 05:31:34 INF CastSponsorSkip version=v0.8.0 commit=9e65ac39
2024-10-22 05:31:34 INF Searching for devices...
2024-10-22 05:31:34 INF Connected to cast device. device="ファミリー ルーム 2"
2024-10-22 05:32:04 INF Video ID not set. Searching YouTube for video ID... device="ファミリー ルーム 2"
2024-10-22 05:32:05 INF Detected video stream. device="ファミリー ルーム 2" video_id=xxx
2024-10-22 05:32:05 INF No segments found for video. device="ファミリー ルーム 2" video_id=xxx

最後に

Android TV for Chromecast の場合には YOUTUBE_API_KEY が必要になるようです

Youtube へリクエストは Chromecast 上で動画が再生されるたびにコールされるのでクオータには注意しましょう

2024年10月25日金曜日

iSponsorBlockTV を Chromecast with GoogleTV で設定する

iSponsorBlockTV を Chromecast with GoogleTV で設定する

概要

RaspberryPi5 上で動作させます

環境

  • RaspberryPi5
  • Raspbian (bookwarm aarch64)
  • docker 27.3.1
  • iSponsorBlockTV 2.2.1
  • Chromecast with GoogleTV

設定ファイル作成

ペアリングなどもここで行います
なお Chromecast with GoogleTV に接続する場合自動で検出してくれないので一度 Chromecast with GoogleTV で Youtube アプリを開き

設定 -> テレビコードでリンク

で RaspberryPi とペアリングするためのコードを表示しておきす

  • docker run --rm -it -v $(pwd)/data:/app/data ghcr.io/dmunozv04/isponsorblocktv --setup-cli

各種設定項目は以下の通りです
123456789012 の部分は Chromecast with GoogleTV に表示されている番号を入力してください

Could not load config file
Blank config file created
Welcome to the iSponsorBlockTV cli setup wizard
Paired with 0 Device(s). Add more? (y/N) y
Enter pairing code (found in Settings - Link with TV code): 123456789012
Pairing...
Paired device: YouTube on TV
Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x7fff87e87190>
Unclosed connector
connections: ['[(<aiohttp.client_proto.ResponseHandler object at 0x7fff87e41b70>, 10237.691475742)]']
connector: <aiohttp.connector.TCPConnector object at 0x7fff87e86f90>
Paired with 1 Device(s). Add more? (y/N) N
API key only needed for the channel whitelist function. Add it? (y/N) N
Enter skip categories (space or comma sepparated) Options: [sponsor, selfpromo, exclusive_access, interaction, poi_highlight, intro, outro, preview, filler, music_offtopic]:
sponsor, selfpromo, exclusive_access, interaction, poi_highlight, intro, outro, preview, filler, music_offtopic
Do you want to whitelist any channels from being ad-blocked? (y/N) N
Do you want to report skipped segments to sponsorblock. Only the segment UUID will be sent? (Y/n) n
Do you want to mute native YouTube ads automatically? (y/N) y
Do you want to skip native YouTube ads automatically? (y/N) y
Do you want to enable autoplay? (Y/n) Y
Config finished

config.json というファイルができていれば OK です

ls data/
config.json

iSponsorBlockTV の起動

  • vim compose.yaml
services:
  iSponsorBlockTV:
    image: ghcr.io/dmunozv04/isponsorblocktv
    container_name: iSponsorBlockTV
    restart: unless-stopped
    volumes:
      - /home/user01/data:/app/data
  • docker compose up -d

ログ

正常に動作しているログは以下の通りです
トークンやチャネルIDはマスクしています

iSponsorBlockTV  | 2024-10-22 06:25:46,392 - iSponsorBlockTV-xxx - INFO - Starting device
iSponsorBlockTV  | Unclosed client session
iSponsorBlockTV  | client_session: <aiohttp.client.ClientSession object at 0x7fff7f9f33d0>
iSponsorBlockTV  | 2024-10-22 06:25:46,531 - iSponsorBlockTV-xxx - INFO - Refreshed auth, lounge id token AGdxxx
iSponsorBlockTV  | 2024-10-22 06:25:46,991 - iSponsorBlockTV-xxx - INFO - Connected to device YouTube on TV (YouTube on TV)
iSponsorBlockTV  | 2024-10-22 06:25:46,991 - iSponsorBlockTV-xxx - INFO - Subscribing to lounge
iSponsorBlockTV  | 2024-10-22 06:25:46,994 - iSponsorBlockTV-xxx - INFO - Subscribing to lounge id AGdxxx
iSponsorBlockTV  | 2024-10-22 06:26:17,875 - iSponsorBlockTV-xxx - INFO - Ad has started, muting
iSponsorBlockTV  | 2024-10-22 06:26:17,878 - iSponsorBlockTV-xxx - INFO - Getting segments for next video: xxx
iSponsorBlockTV  | 2024-10-22 06:26:17,879 - iSponsorBlockTV-xxx - INFO - Ad has started, muting
iSponsorBlockTV  | 2024-10-22 06:26:18,975 - iSponsorBlockTV-xxx - INFO - Getting segments for next video: xxx
iSponsorBlockTV  | 2024-10-22 06:26:19,749 - iSponsorBlockTV-xxx - INFO - Getting segments for next video: xxx
iSponsorBlockTV  | 2024-10-22 06:26:33,071 - iSponsorBlockTV-xxx - INFO - Ad has ended, unmuting
iSponsorBlockTV  | 2024-10-22 06:26:33,827 - iSponsorBlockTV-xxx - INFO - Ad has started, muting
iSponsorBlockTV  | 2024-10-22 06:26:33,827 - iSponsorBlockTV-xxx - INFO - Getting segments for next video: xxx
iSponsorBlockTV  | 2024-10-22 06:26:33,828 - iSponsorBlockTV-xxx - INFO - Ad has started, muting
iSponsorBlockTV  | 2024-10-22 06:26:39,983 - iSponsorBlockTV-xxx - INFO - Ad has ended, unmuting
iSponsorBlockTV  | 2024-10-22 06:26:40,454 - iSponsorBlockTV-xxx - INFO - Ad has started, muting
iSponsorBlockTV  | 2024-10-22 06:26:40,455 - iSponsorBlockTV-xxx - INFO - Getting segments for next video: xxx
iSponsorBlockTV  | 2024-10-22 06:26:40,456 - iSponsorBlockTV-xxx - INFO - Ad has started, muting
iSponsorBlockTV  | 2024-10-22 06:26:45,501 - iSponsorBlockTV-xxx - INFO - Ad can be skipped, skipping
iSponsorBlockTV  | 2024-10-22 06:26:46,049 - iSponsorBlockTV-xxx - INFO - Ad has started, muting
iSponsorBlockTV  | 2024-10-22 06:26:46,797 - iSponsorBlockTV-xxx - INFO - Ad has started, muting
iSponsorBlockTV  | 2024-10-22 06:26:46,798 - iSponsorBlockTV-xxx - INFO - Getting segments for next video: xxx
iSponsorBlockTV  | 2024-10-22 06:26:46,799 - iSponsorBlockTV-xxx - INFO - Ad has started, muting
iSponsorBlockTV  | 2024-10-22 06:26:51,823 - iSponsorBlockTV-xxx - INFO - Ad can be skipped, skipping
iSponsorBlockTV  | 2024-10-22 06:26:52,169 - iSponsorBlockTV-xxx - INFO - Ad has started, muting
iSponsorBlockTV  | 2024-10-22 06:26:55,607 - iSponsorBlockTV-xxx - INFO - Playing video xxx with 0 segments

自動でミュートしたり解除したりしてくれます

最後に

castsponsorskip よりこちらのほうが優秀なのではという気がします

参考サイト

2024年10月24日木曜日

(サンプルコード) SwiftUI で RSS フィードリーダ

(サンプルコード) SwiftUI で RSS フィードリーダ

概要

ポイントは

  • Swift 純正の XMLParser と XMLParserDelegate を使い RSS 情報をパースする
  • XMLParser で CDATA があるときは専用のメソッドを使う
  • CDATA 内にある html をパースして更に必要な情報が取得できる
  • XML の必要な情報はモデルにして SwiftUI 上でバインドする

環境

  • macOS 15.0.1
  • Xcode 16.0

RSSFeedParser.swift

RSS フィードをパースする処理を管理するクラスです

import Foundation
import Combine

class RSSFeedParser: NSObject, ObservableObject, XMLParserDelegate {
    @Published var items: [RSSItem] = []
    
    // 各種要素を管理する変数
    private var currentElement = ""
    private var currentTitle = ""
    private var currentLink = ""
    private var currentDescription = ""
    private var currentContent = ""
    private var parsingItem = false
    
    // RSS フィールドをロードしパースを開始する
    func loadRSSFeed(url: URL) {
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            guard let data = data else { return }
            
            let parser = XMLParser(data: data)
            parser.delegate = self
            parser.parse()
        }
        task.resume()
    }
    
    // XMLParser の Delegate メソッド、要素が見つかったら最初にコールされる
    func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
        currentElement = elementName
        if elementName == "item" {
            currentTitle = ""
            currentLink = ""
            currentDescription = ""
            currentContent = ""
            parsingItem = true
        }
    }
    
    // XMLParser の Delegate メソッド、要素内の一般タグでも見つかったらコールされる
    func parser(_ parser: XMLParser, foundCharacters string: String) {
        if parsingItem {
            if currentElement == "title" {
                currentTitle += string
            } else if currentElement == "link" {
                currentLink += string
            } else if currentElement == "description" {
                currentDescription += string
            }
        }
    }
    
    // XMLParser の Delegate メソッド、要素内の CDATA タグでも見つかったらコールされる
    func parser(_ parser: XMLParser, foundCDATA CDATABlock: Data) {
        if parsingItem && currentElement == "content:encoded" {
            if let cdataString = String(data: CDATABlock, encoding: .utf8) {
                currentContent += cdataString
            }
        }
    }
    
    // XMLParser の Delegate メソッド、要素が終了したらコールされる
    func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
        if elementName == "item" {
            let imageSrc = extractImageSrc(from: currentContent)
            let item = RSSItem(title: currentTitle, link: currentLink, description: currentDescription, content: currentContent, imageSrc: imageSrc)
            DispatchQueue.main.async {
                self.items.append(item)
            }
            parsingItem = false
        }
    }
  
    // CDATA から特定の html 属性情報を抽出するヘルパーメソッド
    func extractImageSrc(from htmlString: String) -> String? {
        let pattern = "<img[^>]+src=\"([^\"]+)\""
        
        if let regex = try? NSRegularExpression(pattern: pattern, options: []) {
            let range = NSRange(htmlString.startIndex..., in: htmlString)
            if let match = regex.firstMatch(in: htmlString, options: [], range: range) {
                if let srcRange = Range(match.range(at: 1), in: htmlString) {
                    return String(htmlString[srcRange])
                }
            }
        }
        return nil
    }
}

RSSItem.swift

RSS フィードをパースした結果のデータを管理するモデルクラスです

import Foundation
import SwiftUI

struct RSSItem: Identifiable {
    let id = UUID()
    let title: String
    let link: String
    let description: String
    let content: String  // html 付きの description
    let imageSrc: String?
}

ContentView.swift

SwiftUI で RSS フィードのモデル情報を表示します

import SwiftUI

struct ContentView: View {
    @StateObject private var rssFeedParser = RSSFeedParser()
    
    var body: some View {
        NavigationView {
            List(rssFeedParser.items) { item in
                VStack(alignment: .leading) {
                    Text(item.title)
                        .font(.headline)
                    if let imageSrc = item.imageSrc {
                        AsyncImage(url: URL(string: imageSrc)) { image in
                            image
                                .resizable()
                                .aspectRatio(contentMode: .fit)
                                .frame(height: 200)
                        } placeholder: {
                            ProgressView()
                        }
                    }
                    Text(item.description)
                        .font(.body)
                    Text(item.link)
                        .font(.subheadline)
                        .foregroundColor(.blue)
                }
            }
            .navigationTitle("RSS Feed")
            .onAppear {
                if let url = URL(string: "https://figmanendoroiddb.blog.fc2.com/?xml") { // RSS URLをここに設定
                    rssFeedParser.loadRSSFeed(url: url)
                }
            }
        }
    }
}

#Preview {
    ContentView()
}

testApp.swift

実行メインクラス

import SwiftUI

@main
struct testApp: App {  
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

最後に

SwiftUI を使った場合 UI とモデルのデータバインドがほぼ自動でやってくれるので楽です

表示するコンテンツの情報も SwiftUI 側で調整するだけで RSS フィードを取得する側のロジックでは全く気にする必要がないです