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 のインストールがやがら遅い気がする

参考サイト