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 フィードを取得する側のロジックでは全く気にする必要がないです

2024年10月23日水曜日

SwiftUI + BackgroundTasks で定期的にローカルプッシュを送信する方法

SwiftUI + BackgroundTasks で定期的にローカルプッシュを送信する方法

概要

アプリがバックグランドに移行した際にバックグランドタスクを使って定期的にローカルプッシュを送信してみます

環境

  • macOS 15.0.1
  • Xcode 16.0

Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>BGTaskSchedulerPermittedIdentifiers</key>
        <array>
                <string>com.example.app.refresh</string>
        </array>
        <key>UIBackgroundModes</key>
        <array>
                <string>fetch</string>
        </array>
</dict>
</plist>

サンプルコード

ポイントはタスクのイベントハンドラで (handleAppRefresh) で再キューイングしている部分とタスクが実行された際にローカルプッシュを送信している部分です

import SwiftUI
import BackgroundTasks

@main
struct testApp: App {
    // SwiftUI における AppDelegateの設定
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    @Environment(\.scenePhase) private var scenePhase
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) {
            if scenePhase == .background {
                // バックグラウンドに入る際にタスクをスケジュール
                appDelegate.scheduleAppRefresh()
            }
        }
    }
}

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // 通知の許可
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
            if granted {
                print("Notification permission granted")
            } else if let error = error {
                print("Notification permission error: \(error.localizedDescription)")
            }
        }
        // バックグラウンドタスクの登録、ここのコールバックメソッドは実際にタスクが実行される際に呼ばれます
        BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.example.app.refresh", using: nil) { task in
            self.handleAppRefresh(task: task as! BGAppRefreshTask)
        }
        return true
    }
    
    func scheduleAppRefresh() {
        let request = BGAppRefreshTaskRequest(identifier: "com.example.app.refresh")
        // 10秒後に実行するようにしているが実際に実行されるのはアプリがバックグランドに移動してからタスクが実行可能になった10秒後に実行される
        // バックグランドに移動してから10秒後ではないので注意
        request.earliestBeginDate = Date(timeIntervalSinceNow: 10)
        do {
            try BGTaskScheduler.shared.submit(request)
        } catch {
            print("Unable to schedule app refresh: \(error)")
        }
    }
    
    // 実際にタスクが実行された際にタスクの実行結果のハンドリングを行うメソッド
    func handleAppRefresh(task: BGAppRefreshTask) {
        task.expirationHandler = {
            print("Task expired")
        }
        let operation = BlockOperation {
            // ローカルプッシュを送信
            self.sendLocalNotification()
            print("Background task is running")
        }
        operation.completionBlock = {
            task.setTaskCompleted(success: !operation.isCancelled)
            print("Task completed: \(operation.isCancelled ? "Cancelled" : "Success")")
            // タスク完了後、次のタスクをスケジュール
            self.scheduleAppRefresh()
        }
        OperationQueue().addOperation(operation)
    }
    
    // ローカルプッシュ行うメソッド
    func sendLocalNotification() {
        let content = UNMutableNotificationContent()
        content.title = "Background Task"
        content.body = "This is a local notification from the background task."
        content.sound = .default
        
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
        let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
        
        UNUserNotificationCenter.current().add(request) { error in
            if let error = error {
                print("Error in scheduling local notification: \(error.localizedDescription)")
            }
        }
    }
}

最後に

iPhone 側のリソース状況にもよりますがだいたい 10分から15分に1回ローカルプッシュが来るようになります
あくまでもアプリがバックグランドにいる場合なのでキルされた場合などは来ないのでご注意ください

2024年10月22日火曜日

Minecraft server をクリエイティブモードで起動する方法

Minecraft server をクリエイティブモードで起動する方法

概要

過去にdocker でマイクラサーバを起動しました
今回はクリエイティブモードで起動する方法を紹介します

環境

  • macOS 15.0.1
  • docker 27.2.0
  • itzg/minecraft-server 2024.10.1

compose.yaml ファイル

environment に MODE=creative を追加します
すでにワールドがある場合には再度ワールドを作成し直します
docker の場合は data ディレクトリを作成し直せば OK です
前のワールドを残したい場合はディレクトリをリネームしておきましょう

services:
  mc:
    image: itzg/minecraft-server
    tty: true
    stdin_open: true
    ports:
      - "25565:25565"
    environment:
      EULA: "TRUE"
      MODE: "creative"
    volumes:
      # attach the relative directory 'data' to the container's /data path
      - ./data:/data

最後に

他にもいろいろな設定を環境変数で入れられるようです
詳細は以下のリンクを御覧ください

参考サイト

2024年10月21日月曜日

SwiftUI + BackgroundTasks を試してみた

SwiftUI + BackgroundTasks を試してみた

概要

SwiftUI で Delegate を定義してその中でバックグランド処理を定義します
アプリがバックグランドに移動するとタスクがキューイングされ実行されます

Swift の BackgroundTasks の最大のポイントは自分で実行する時間を制御できない点です
iOS 側のリソース状況によって実行時間がだいぶ変わるのですぐには実行されません

環境

  • macOS 15.0.1
  • Xcode 16.0

Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>BGTaskSchedulerPermittedIdentifiers</key>
        <array>
                <string>com.example.app.refresh</string>
        </array>
        <key>UIBackgroundModes</key>
        <array>
                <string>fetch</string>
        </array>
</dict>
</plist>

サンプルコード

実際にタスクが実行されるのは iOS 側の制御になるので指定の時間や指定の間隔で必ず実行されないことに注意です

import SwiftUI
import BackgroundTasks

@main
struct testApp: App {
    // SwiftUI における AppDelegateの設定
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    @Environment(\.scenePhase) private var scenePhase
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) {
            if scenePhase == .background {
                // バックグラウンドに入る際にタスクをスケジュール
                appDelegate.scheduleAppRefresh()
            }
        }
    }
}

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // バックグラウンドタスクの登録、ここのコールバックメソッドは実際にタスクが実行される際に呼ばれます
        BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.example.app.refresh", using: nil) { task in
            self.handleAppRefresh(task: task as! BGAppRefreshTask)
        }
        return true
    }
    
    func scheduleAppRefresh() {
        let request = BGAppRefreshTaskRequest(identifier: "com.example.app.refresh")
        // 10秒後に実行するようにしているが実際に実行されるのはアプリがバックグランドに移動してからタスクが実行可能になった10秒後に実行される
        // バックグランドに移動してから10秒後ではないので注意
        request.earliestBeginDate = Date(timeIntervalSinceNow: 10)
        do {
            try BGTaskScheduler.shared.submit(request)
        } catch {
            print("Unable to schedule app refresh: \(error)")
        }
    }
    
    // 実際にタスクが実行された際にタスクの実行結果のハンドリングを行うメソッド
    func handleAppRefresh(task: BGAppRefreshTask) {
        task.expirationHandler = {
            print("Task expired")
        }
        let operation = BlockOperation {
            print("Background task is running")
        }
        operation.completionBlock = {
            task.setTaskCompleted(success: !operation.isCancelled)
            print("Task completed: \(operation.isCancelled ? "Cancelled" : "Success")")
        }
        OperationQueue().addOperation(operation)
    }
}

最後に

今回のサンプルコードではアプリがバックグランドに移動するたびにタスクがキューイングされるので定期的に実行されるようなタスクではありません

そもそも iOS ではバックグランド処理はリソースを占有する処理になるため悪とされているので可能な限り使わないほうがいいのかなと思います

Debug -> Simulate Background fetch で実行して 15 分後くらいにデバッグログが表示されました

2024年10月20日日曜日

uYouEnhanced の LSA 対策方法

uYouEnhanced の LSA 対策方法

概要

エラーの詳細は以下です

google で安全性を確認できないため、このアプリにはログインできません。

環境

  • uYouEnhanced 19.40.4
  • iOS 18.0.1

LSA 対策してログインする方法

  1. uYouEnhanced_19.16.3-3.0.3.ipa をダウンロード
    • starfiles などで OK
  2. ipa ファイルを iPhone 側に送る
    • iPhone で直接 ipa をダウンロードした場合は不要
    • ipa を送る際に AirDrop はダメ
  3. AltStore で MyApps のプラスボタンから ipa を指定して直接インストール
  4. インストールした uYouEnhanced (ver19.16.3) を開き一度 Google アカウントでログインする
  5. AltStore で 以下のSources を追加し最新版を再度インストール
  6. 最新版の uYouEnhanced を開きログインできていることを確認する

最後に

AltStore だと uYouEnhanced のインストールがやがら遅い気がする

参考サイト

2024年10月19日土曜日

terraform の nifcloud provider を使って Gitlab Runner を構築してみた

terraform の nifcloud provider を使って Gitlab Runner を構築してみた

概要

こちらを使ってみました

環境

  • Ubuntu 24.04
  • terraform 1.9.7
    • nifcloud provider 1.16

インストール

sudo apt update && sudo apt install -y gnupg software-properties-common
wget -O- https://apt.releases.hashicorp.com/gpg | \

gpg --dearmor | \
sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null

gpg --no-default-keyring \
--keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg \
--fingerprint

echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list

sudo apt update
sudo apt install terraform

terraform -install-autocomplete

事前準備

Gitlab 側の任意のプロジェクトで Runner を登録する準備をしましょう
glrt- から始まるトークンが取得できれば OK です
このトークンは後述する variables.tf の token 変数として使われ環境変数から読み込みます

また今回は nifcloud を使うので nifcloud のアクセスキーとシークレットキーを準備しておいてください

main.tf

今回は3つのリソースを作成します

  • Runner 本体
  • Runner パラメータグループ
  • Runner と Gitlab の紐づけ

流れとして Runner 本体を作成後にパラメータグループを作成しそのパラメータグループを使って Runner と Gitlab を紐づけます

今回は紐づける Gitlab は https://gitlab.com をデフォルトの値にしています

resource "nifcloud_devops_runner" "test" {
  name              = var.runner_name
  instance_type     = var.instance_type
  availability_zone = var.az
  concurrent        = var.concurrent
  description       = var.description
}

resource "nifcloud_devops_runner_parameter_group" "test" {
  name = var.parameter_group_name
}

resource "nifcloud_devops_runner_registration" "group_runner" {
  runner_name          = var.runner_name
  gitlab_url           = var.gitlab_url
  parameter_group_name = var.parameter_group_name
  # 事前にgitlabからrunnerを登録するためのトークンを取得しておくこと
  token = var.token
}

variables.tf

変数を定義します
各種変数には default を用意していますが必要に応じて変更もしくは環境変数 TF_VAR_xxx から上書きしてもらって OK です

token は環境変数を使って挿入するので default はありません

variable "runner_name" {
  default = "test-runner"
  type    = string
}

variable "instance_type" {
  default = "c-small"
  type    = string
}

variable "az" {
  default = "east-11"
  type    = string
}

variable "concurrent" {
  default = 10
  type    = number
}

variable "description" {
  default = "memo"
  type    = string
}

variable "parameter_group_name" {
  default = "test-pg"
  type    = string
}

variable "gitlab_url" {
  default = "https://gitlab.com"
  type = string
}

variable "token" {
  type      = string
  sensitive = true
}

providers.tf

nifcloud プロバイダを使う定義を記載してるだけです
リージョンなど必要に応じて変更してください

terraform {
  required_providers {
    nifcloud = {
      source = "nifcloud/nifcloud"
    }
  }
}

provider "nifcloud" {
  region = "jp-east-1"
}

注意事項

自身 Gitlab と作成した Runner が疎通できていないといけないので一度実行して Runner を作成したあとに IP が払い出されるんのでその IP を Gitlab のファイアウォールに追加してあげてから再度 apply すれば Runner 登録も完了します

動作確認

  • terraform init
  • TF_VAR_token=glrt-xxx NIFCLOUD_ACCESS_KEY_ID=xxx NIFCLOUD_SECRET_ACCESS_KEY=xxx terraform plan
  • TF_VAR_token=glrt-xxx NIFCLOUD_ACCESS_KEY_ID=xxx NIFCLOUD_SECRET_ACCESS_KEY=xxx terraform apply

Gitlab 側から Runner が登録されていれば OK です

最後に

Runner トークンを払い出すのが面倒ですがそれもうまくいけば自動化できるかなと思います

現状は複数のトークンを指定することはできませんがうまくやれば count と組み合わせて一括登録的なこともできるかなと思います

参考サイト

2024年10月18日金曜日

iPhone で UTM を使って Ubuntu を起動してみる

iPhone で UTM を使って Ubuntu を起動してみる

概要

AltStore + UTM で試してみました
JIT が使えないと恐ろしく遅いので JIT を有効化してから UTM を起動します

環境

  • iPhone14 (iOS 18.0.1)
  • macOS 15.0.1
  • AltServer 1.7.2
  • UTM (install via AltStore)
  • Python 3.9.6 (system installed)
  • Ubuntu 24.04 LTS

pymobiledevice3 のインストール

  • pyenv global system
  • python3 -m pip install -U pymobiledevice3==2.30.0
  • python3 -m pip install -U construct==2.10.69
  • python3 -m pip install -U qh3==0.15.1

iPhone と Mac の有線接続

今のところ JIT の有効化は有線のみサポートしているようです

JIT の有効化

AltServer を起動し有効にします

AltServer -> Enable JIT -> iPhone -> UTM

有効にできると以下のようなメッセージが出てきます
また権限を許可するダイアログが途中で表示されるので許可します

Ubuntu イメージのダウンロード

Ubuntu の iso イメージをダウンロードします
Ubuntu の公式から iPhone でダウンロードします

ファイル名は ubuntu-24.04.1-live-server-amd64.iso でファイルサイズは約2.5GB でした

UTM の起動とエミュレートの作成

UTM アプリを起動します
iPhone には Linux カーネルがなくVM は作成できないのでエミュレートで追加します

  • 左上プラスボタン
  • エミュレート
  • Linux
  • カーネルイメージから起動 -> オフ
    • 起動 ISO イメージ -> 選択 -> ubuntu-24.04.1-live-server-amd64.iso を選択
    • iso は Safari でダウンロードした場合は iCloud 内にあります
  • マシン設定
    • アーキテクチャ -> ARM64
    • システム -> Standard PC (q35)
    • メモリ -> 512MiB
    • CPU コア数 -> 4
    • ディスクサイズ -> 8GB
    • 共有ディレクトリ -> なし

Ubuntu 起動

あとは一覧から起動するだけです
事前に Bluetooth キーボードなどを設定しておくと操作が楽になります

「Display output is not active」が表示されますがしばらくすると起動するので待っていれば OK です

cloud-init の実行が完了すると以下のように Ubuntu のインストールが開始されます

最後に

AltServer の JIT 機能を使って iPhone 上で Ubuntu をエミューレートしてみました
JIT を使えば動かないことはないですがかなり遅い感じはします
また作業中はかなり iPhone が熱くなるので注意が必要です

もっと軽量でインストール不要の OS ならサクサク動くかもしれません
あとは iPhone14 ではなく iPhone15 や Pro Max などスペックの高い iPhone なら動くかもしれません

参考サイト

2024年10月17日木曜日

東京メトロの時刻表は JSON API で取得できる

東京メトロの時刻表は JSON API で取得できる

概要

取得方法を紹介します

環境

  • macOS 15.0.1
  • curl 8.10.0

方法

  • curl https://transfer.tokyometro.jp/api/timetable?numbering=M25&lang=ja

長いの json の内容は省略しますがだいたい以下の情報が含まれています

  • 駅名
  • 行先き (上下線)
  • 行先きに対する時刻表 (平日/休日)
  • 時刻表は以下のデータが含まれています
{
  "time": "2024-10-07T05:00:00+09:00",
  "id": "82430103",
  "train_no": "A501",
  "type": "",
  "train_name": "",
  "departure": true,
  "previous_departure": false,
  "color": "#000000",
  "destinations": [
    "荻窪"
  ]
}

最後に

Web スクレーピングする必要がないので結構便利です

2024年10月16日水曜日

iOS アプリの entitlements を確認する方法

iOS アプリの entitlements を確認する方法

概要

コマンドで取得する方法を紹介します
ipa ファイルではなく app ファイルに対して行うのがポイントです

環境

  • macOS 15.0.1
  • Xcode 16.0

コマンド

  • codesign -d --entitlements :- app_name.app | xmllint --format -
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>application-identifier</key>
    <string>your.app.identifier</string>
    <key>com.apple.developer.team-identifier</key>
    <string>xxxxxx</string>
    <key>get-task-allow</key>
    <true/>
  </dict>
</plist>

Xcode であれば

~/Library/Developer/Xcode/DerivedData/app_name-cuzoqdebxqlaksegeoxfmmsaetqz/Build/Products/Debug-iphoneos/app_name.app

に archive ビルドした app ファイルがあります

2024年10月15日火曜日

サブアカウント用の Apple ID を新規で作成する方法

サブアカウント用の Apple ID を新規で作成する方法

概要

ここ https://account.apple.com/account から作成するだけです

環境

  • macOS 15.0.1

ポイント

  • 異なるメールアドレスが必要
  • 電話番号は同一でも OK

最後に

サブアカウントが必要になるケースがあるのでその場合は作成しましょう

2024年10月14日月曜日

朝の目覚ましにプレイリストのランダム再生を使うオートメーションの設定

朝の目覚ましにプレイリストのランダム再生を使うオートメーションの設定

概要

忘れるのでメモ

環境

  • iPhone 14
  • iOS 18.0.1

アクションの内容

上から順番にアクション名は

  • 音量を設定
  • プレイリストを取得
  • ミュージックを検索
  • ミュージックを再生

最後に

iOS のバージョンによってアクション名の表記に若干違いがある可能性があるので注意です

参考サイト

2024年10月13日日曜日

iOS アプリが必要としているパーミッションの一覧を取得する方法

iOS アプリが必要としているパーミッションの一覧を取得する方法

概要

アプリがどんなパーミッションを必要としているか確認するときに使えます
おまけでバージョン各種バージョン情報を取得するコマンドも掲載します

環境

  • macOS 15.0.1
  • Xcode 16.0

コマンド

for i in `find . -name "Info.plist" | grep -v 'Pods/' | grep -v 'Tests'`; do echo ${i}; plutil -p ${i} | grep 'UsageDescription'; done

CFBundleVersion を確認するコマンド

for i in `find . -name "Info.plist" | grep -v 'Pods/' | grep -v 'Tests'`; do echo ${i}; plutil -p ${i} | grep 'CFBundleVersion'; done

or

for i in `find . -name "*.pbxproj" | grep -v 'Pods/' | grep -v 'Tests'`; do echo ${i}; plutil -p ${i} | grep 'CURRENT_PROJECT_VERSION'; done

CFBundleShortVersionString を確認するコマンド

for i in `find . -name "Info.plist" | grep -v 'Pods/' | grep -v 'Tests'`; do echo ${i}; plutil -p ${i} | grep 'CFBundleShortVersionString'; done

or

for i in `find . -name "*.pbxproj" | grep -v 'Pods/' | grep -v 'Tests'`; do echo ${i}; plutil -p ${i} | grep 'MARKETING_VERSION'; done

デプロイターゲットバージョンを確認するコマンド

for i in `find . -name "*.pbxproj" | grep -v 'Pods/' | grep -v 'Tests'`; do echo ${i}; plutil -p ${i} | grep 'IPHONEOS_DEPLOYMENT_TARGET'; done

参考サイト