Changed name to unyo-core

This commit is contained in:
Kevin Rodrigues Borges
2026-05-05 00:49:27 +01:00
parent ef6197db62
commit e336275363
126 changed files with 728 additions and 92 deletions

View File

@@ -8,8 +8,8 @@ Java/Kotlin Maven multi-module project that provides a JVM runtime for Aniyomi (
# Build fat JAR (includes all dependencies)
mvn clean package
# Output: aniyomibridge-core/target/aniyomibridge-core-1.0.0.jar
# Main class: dev.k3vinb5.aniyomibridge.Main
# Output: aniyomi-bridge/target/aniyomiBridge-1.0.0.jar
# Main class: dev.k3vinb5.unyo.core.cli.Main
```
## Project Structure
@@ -17,14 +17,14 @@ mvn clean package
This is a multi-module Maven project with the following modules:
```
aniyomibridge-core/
unyo-core/
├── pom.xml # Parent POM
├── androidcompat/ # Android compatibility layer
│ ├── src/main/java/android/ # Custom Android stub implementations
│ ├── src/main/java/androidx/ # AndroidX stub implementations
│ ├── scripts/get-android.sh # Android SDK JAR download script
│ └── pom.xml
├── aniyomibridge-core/ # Core bridge module
├── aniyomi-bridge/ # Core bridge module
│ ├── src/main/java/ # Java sources (bridge core)
│ ├── src/main/kotlin/ # Kotlin sources (Tachiyomi/Aniyomi interfaces)
│ └── pom.xml
@@ -65,7 +65,7 @@ To enable:
1. Update `androidcompat/pom.xml` - change `<phase>none</phase>` to `<phase>generate-resources</phase>`
2. Or manually download `android.jar` and place in `androidcompat/target/`
### Module: `aniyomibridge-core`
### Module: `aniyomi-bridge`
Core bridge module that loads and executes extensions.
@@ -91,7 +91,7 @@ Core bridge module that loads and executes extensions.
**Maven Shade Configuration**:
- Excludes signing artifacts (SF/DSA/RSA) and module-info
- Excludes `Main.class` from the JAR (consumed by external Flutter app)
- Manifest points to `dev.k3vinb5.aniyomibridge.cli.Main`
- Manifest points to `dev.k3vinb5.unyo.core.cli.Main`
## Key Dependencies
@@ -102,7 +102,7 @@ Core bridge module that loads and executes extensions.
- `jsoup` - HTML parsing
- `quickjs-kt-jvm` - JavaScript execution
### aniyomibridge-core
### aniyomi-bridge
- `androidcompat` - Android compatibility layer (this project)
- `dex2jar` - Converts Android APK DEX → JAR
- `kotlinx-serialization-json` - JSON handling
@@ -118,7 +118,7 @@ Core bridge module that loads and executes extensions.
| `generate-new-core-mac.sh` | Build + copy JAR to Flutter app assets (Mac path) |
| `androidcompat/scripts/get-android.sh` | Download and patch Android SDK JAR |
Both build scripts copy to a sibling project `k3vinb5_aniyomi_bridge/assets/`.
Both build scripts copy to a sibling project `unyo-lib/assets/`.
## Android Stubs Strategy
@@ -179,7 +179,7 @@ If you're migrating from the old single-module structure:
1. Android stubs moved from `src/main/java/android/` to `androidcompat/src/main/java/android/`
2. Build command unchanged: `mvn clean package`
3. Output location unchanged: `aniyomibridge-core/target/`
3. Output location unchanged: `aniyomi-bridge/target/`
4. New dependency: Core module now depends on `androidcompat` module
## Future Improvements

625
JNI_SPECIFICATION.md Normal file
View File

@@ -0,0 +1,625 @@
# Unyo Core JNI Specification
> **Version:** 2.0 (Post-Multi-Module Refactor)
> **Target:** Flutter JNI integration for `unyo-core-1.0.0.jar`
> **Main Class:** `dev.k3vinb5.unyo.core.AniyomiBridge`
---
## Overview
This document specifies the public Java API exposed by `AniyomiBridge.java` for consumption via JNI from Flutter. It covers every method, its signature, the data models that cross the JNI boundary, and the behavioral changes introduced during the multi-module refactor.
**Key architectural fact:** `AniyomiBridge` is a **singleton-like stateful object**. It maintains internal maps of loaded anime/manga sources keyed by `packageName`. You create one instance and call methods on it. All source lookup is done internally via these maps.
---
## Table of Contents
1. [Initialization](#1-initialization)
2. [Extension Lifecycle (Anime)](#2-extension-lifecycle-anime)
3. [Extension Lifecycle (Manga)](#3-extension-lifecycle-manga)
4. [Querying Loaded Extensions](#4-querying-loaded-extensions)
5. [Preferences API](#5-preferences-api)
6. [Content Fetching (Anime)](#6-content-fetching-anime)
7. [Content Fetching (Manga)](#7-content-fetching-manga)
8. [Data Models](#8-data-models)
9. [Error Handling](#9-error-handling)
10. [Breaking Changes from v1](#10-breaking-changes-from-v1)
11. [JNI Implementation Notes](#11-jni-implementation-notes)
---
## 1. Initialization
### `static void init()`
Must be called **once** before any other operation. Performs global JVM-level setup:
- Installs ByteBuddy agent for runtime bytecode patching
- Initializes Injekt dependency injection module
**JNI signature:**
```
(env)->CallStaticVoidMethod(aniyomiBridgeClass, initMethod);
```
**Thread safety:** Safe to call from any thread, but should only be called once. Not synchronized externally — caller must ensure single invocation.
**Throws:** Nothing directly, but ByteBuddy installation can fail if the JVM doesn't support instrumentation.
---
## 2. Extension Lifecycle (Anime)
### `void loadAnimeExtension(String extensionUrl, String basePath, String packageName)`
Downloads an APK from `extensionUrl`, converts it to JAR via dex2jar, loads the main extension class, and registers it as an `AnimeHttpSource`.
| Parameter | Description |
|-----------|-------------|
| `extensionUrl` | Full HTTPS URL to the `.apk` file |
| `basePath` | Local directory path where the converted `.jar` will be cached |
| `packageName` | The Android package name (e.g., `eu.kanade.tachiyomi.animeextension.en.animepahe`). Used as the **internal key** for all subsequent lookups. |
**Behavior:**
- Downloads APK if not already present
- Converts APK → JAR using dex2jar
- Inspects JAR to find the main extension class
- Loads class via isolated `URLClassLoader`
- Applies ByteBuddy patches to the extension's JVM classes
- If the source implements `ConfigurableAnimeSource`, automatically calls `setupPreferenceScreen()` with a managed `PreferenceScreen`
- Stores the source in an internal `ConcurrentHashMap` keyed by `packageName`
**Throws:** `UnyoCoreException` on any failure (download, dex2jar, class loading, casting).
---
### `void unloadAnimeExtension(String packageName)`
Unloads an anime extension and cleans up resources.
| Parameter | Description |
|-----------|-------------|
| `packageName` | The same `packageName` passed to `loadAnimeExtension` |
**Behavior:**
- Closes the extension's `URLClassLoader`
- Deletes the cached JAR file from disk
- Removes the source from internal maps
**Throws:** `UnyoCoreException` on cleanup failure.
---
## 3. Extension Lifecycle (Manga)
### `void loadMangaExtension(String extensionUrl, String basePath, String packageName)`
Identical to `loadAnimeExtension` but for manga sources implementing `CatalogueSource`.
| Parameter | Description |
|-----------|-------------|
| `extensionUrl` | Full HTTPS URL to the `.apk` file |
| `basePath` | Local directory path where the converted `.jar` will be cached |
| `packageName` | Android package name, used as internal key |
**Behavior:** Same as anime version, but casts to `CatalogueSource` and calls `ConfigurableSource.setupPreferenceScreen()` if applicable.
**Throws:** `UnyoCoreException`
---
### `void unloadMangaExtension(String packageName)`
Unloads a manga extension.
| Parameter | Description |
|-----------|-------------|
| `packageName` | The same `packageName` passed to `loadMangaExtension` |
**Throws:** `UnyoCoreException`
---
## 4. Querying Loaded Extensions
### `List<String> getAnimeLoadedExtensions()`
Returns a list of loaded anime extension package names.
**Return:** `java.util.List<java.lang.String>` — package names of all currently loaded anime sources.
---
### `List<String> getMangaLoadedExtensions()`
Returns a list of loaded manga extension package names.
**Return:** `java.util.List<java.lang.String>` — package names of all currently loaded manga sources.
---
## 5. Preferences API
> **New in v2.** Previously, preferences were not exposed to Flutter. Now they are fully readable and writable.
When a `ConfigurableAnimeSource` or `ConfigurableSource` is loaded, `AniyomiBridge` automatically calls `setupPreferenceScreen(screen)` and creates a `PreferenceScreen` backed by file-based `SharedPreferences` (stored under `~/.aniyomibridge/shared_prefs/<source-id>.xml`).
### `List<PreferenceItem> getAnimePreferences(String packageName)`
Reads all preferences registered by an anime extension.
| Parameter | Description |
|-----------|-------------|
| `packageName` | Package name of the loaded anime extension |
**Return:** `java.util.List<PreferenceItem>`
**Behavior:**
- Returns empty list if source is not configurable
- Iterates all `Preference` objects on the source's `PreferenceScreen`
- Reads current value from `SharedPreferences` (or default if never set)
- Returns typed values based on preference type
**Supported preference types:**
| Type | Java Type | Value Type |
|------|-----------|------------|
| `SwitchPreferenceCompat` | `Boolean` | `true` / `false` |
| `ListPreference` | `String` | Selected entry value |
| `EditTextPreference` | `String` | User input text |
| `MultiSelectListPreference` | `Set<String>` | Selected values |
| `Preference` (fallback) | `null` | No value |
---
### `void setAnimePreference(String packageName, String key, Object value)`
Sets a single preference value for an anime extension.
| Parameter | Description |
|-----------|-------------|
| `packageName` | Package name of the loaded anime extension |
| `key` | Preference key (from `getAnimePreferences`) |
| `value` | Value to set. Must match the expected type: `Boolean`, `String`, or `Set<String>` |
**Throws:** `UnyoCoreException` if source not configurable, key not found, or type mismatch.
---
### `void setAnimePreferences(String packageName, Map<String, Object> values)`
Batch-updates multiple preferences for an anime extension.
| Parameter | Description |
|-----------|-------------|
| `packageName` | Package name of the loaded anime extension |
| `values` | `java.util.Map` of `key → value` pairs |
**Behavior:** Iterates the map and calls `setAnimePreference` logic for each entry. Fails fast on first error.
**Throws:** `UnyoCoreException`
> **Note:** Manga preferences API is **not yet exposed** in `AniyomiBridge.java`. If needed, the same pattern applies using `CatalogueSource` / `ConfigurableSource`.
---
## 6. Content Fetching (Anime)
All content-fetching methods are **synchronous blocking calls** that internally run Kotlin coroutines via `runBlocking`. Call them from background threads in Flutter.
### `List<SAnime> getAnimeSearchResults(String query, int page, String packageName)`
Searches for anime using the specified extension.
| Parameter | Description |
|-----------|-------------|
| `query` | Search query string |
| `page` | Page number (1-based) |
| `packageName` | Package name of the loaded anime extension |
**Return:** `java.util.List<SAnime>`
**Throws:** `UnyoCoreException` wrapping any network or parsing error.
---
### `List<SEpisode> getEpisodeList(SAnime anime, String packageName)`
Fetches the episode list for a given anime.
| Parameter | Description |
|-----------|-------------|
| `anime` | `SAnime` object (from search results or previous call) |
| `packageName` | Package name of the loaded anime extension |
**Return:** `java.util.List<SEpisode>`
**Throws:** `UnyoCoreException`
---
### `List<Video> getVideoList(SEpisode episode, String packageName)`
Fetches video URLs/metadata for a given episode.
| Parameter | Description |
|-----------|-------------|
| `episode` | `SEpisode` object |
| `packageName` | Package name of the loaded anime extension |
**Return:** `java.util.List<Video>`
**Throws:** `UnyoCoreException`
> **Important:** The `Video` class contains `okhttp3.Headers` which is **not serializable by default**. If you need to pass video data across JNI/Flutter isolates, consider using `SerializableVideo` (see Data Models).
---
## 7. Content Fetching (Manga)
### `List<SManga> getMangaSearchResults(String query, int page, String packageName)`
Searches for manga.
| Parameter | Description |
|-----------|-------------|
| `query` | Search query string |
| `page` | Page number (1-based) |
| `packageName` | Package name of the loaded manga extension |
**Return:** `java.util.List<SManga>`
**Throws:** `UnyoCoreException`
---
### `List<SChapter> getChapterList(SManga manga, String packageName)`
Fetches chapter list for a manga.
| Parameter | Description |
|-----------|-------------|
| `manga` | `SManga` object |
| `packageName` | Package name of the loaded manga extension |
**Return:** `java.util.List<SChapter>`
**Throws:** `UnyoCoreException`
---
### `List<Page> getPageList(SChapter chapter, String packageName)`
Fetches image page URLs for a chapter.
| Parameter | Description |
|-----------|-------------|
| `chapter` | `SChapter` object |
| `packageName` | Package name of the loaded manga extension |
**Return:** `java.util.List<Page>`
**Throws:** `UnyoCoreException`
---
## 8. Data Models
### `PreferenceItem`
**Package:** `dev.k3vinb5.unyo.core.PreferenceItem`
**Fields:**
| Field | Type | Description |
|-------|------|-------------|
| `key` | `String` | Unique preference identifier |
| `title` | `String` | Human-readable title |
| `type` | `String` | One of: `SwitchPreferenceCompat`, `ListPreference`, `EditTextPreference`, `MultiSelectListPreference`, `Preference` |
| `value` | `Object` | Current value (`Boolean`, `String`, or `Set<String>`) |
| `entries` | `List<String>` | Display labels (for List/MultiSelect) |
| `entryValues` | `List<String>` | Internal values (for List/MultiSelect) |
| `summary` | `String` | Optional description text |
**JNI tip:** Access fields directly (they are `public`). No getters/setters.
---
### `SAnime`
**Package:** `eu.kanade.tachiyomi.animesource.model.SAnime`
**Interface fields (all `public`):**
| Field | Type | Notes |
|-------|------|-------|
| `url` | `String` | Relative or absolute URL to the anime page |
| `title` | `String` | Display title |
| `artist` | `String?` | Nullable |
| `author` | `String?` | Nullable |
| `description` | `String?` | Nullable |
| `genre` | `String?` | Nullable; comma-separated genres |
| `status` | `int` | `UNKNOWN=0`, `ONGOING=1`, `COMPLETED=2`, `LICENSED=3`, `PUBLISHING_FINISHED=4`, `CANCELLED=5`, `ON_HIATUS=6` |
| `thumbnail_url` | `String?` | Cover image URL |
| `update_strategy` | `AnimeUpdateStrategy` | Enum |
| `fetch_type` | `FetchType` | Enum |
| `season_number` | `double` | Season number |
| `initialized` | `boolean` | Whether full metadata has been fetched |
**Method:** `List<String> getGenres()` — parses `genre` string into a list.
---
### `SEpisode`
**Package:** `eu.kanade.tachiyomi.animesource.model.SEpisode`
**Interface fields:**
| Field | Type | Notes |
|-------|------|-------|
| `url` | `String` | Episode page URL |
| `name` | `String` | Episode title/number |
| `date_upload` | `long` | Unix timestamp (ms) |
| `episode_number` | `float` | Numeric episode number |
| `scanlator` | `String?` | Nullable |
---
### `Video`
**Package:** `eu.kanade.tachiyomi.animesource.model.Video`
**Fields:**
| Field | Type | Notes |
|-------|------|-------|
| `videoUrl` | `String` | Direct video stream URL |
| `videoTitle` | `String` | Quality label (e.g., "1080p") |
| `resolution` | `Integer?` | Nullable resolution in pixels |
| `bitrate` | `Integer?` | Nullable bitrate |
| `headers` | `okhttp3.Headers?` | HTTP headers required for playback |
| `preferred` | `boolean` | Whether this is the preferred quality |
| `subtitleTracks` | `List<Track>` | Subtitle tracks |
| `audioTracks` | `List<Track>` | Audio tracks |
| `timestamps` | `List<TimeStamp>` | Chapter/intro markers |
| `mpvArgs` | `List<Pair<String,String>>` | MPV player arguments |
| `ffmpegStreamArgs` | `List<Pair<String,String>>` | FFmpeg stream arguments |
| `ffmpegVideoArgs` | `List<Pair<String,String>>` | FFmpeg video arguments |
| `internalData` | `String` | Opaque internal data from extractor |
| `initialized` | `boolean` | Whether video metadata is resolved |
**Deprecation notice:** Old extensions may use `quality` (maps to `videoTitle`) and `url` (maps to internal `videoPageUrl`). New code should use `videoTitle` and `videoUrl`.
---
### `SManga`
**Package:** `eu.kanade.tachiyomi.source.model.SManga`
Identical structure to `SAnime` but without `fetch_type`, `season_number`, and `update_strategy` is `UpdateStrategy` instead of `AnimeUpdateStrategy`.
---
### `SChapter`
**Package:** `eu.kanade.tachiyomi.source.model.SChapter`
Identical structure to `SEpisode` but `chapter_number` instead of `episode_number`.
---
### `Page`
**Package:** `eu.kanade.tachiyomi.source.model.Page`
**Fields:**
| Field | Type | Notes |
|-------|------|-------|
| `index` | `int` | Zero-based page index |
| `url` | `String` | Page URL (may be relative) |
| `imageUrl` | `String?` | Resolved direct image URL |
| `number` | `int` | Human-readable page number (`index + 1`) |
---
## 9. Error Handling
### `UnyoCoreException`
**Package:** `dev.k3vinb5.unyo.core.exceptions.UnyoCoreException`
Extends `RuntimeException`.
**Constructors:**
- `UnyoCoreException(String message)`
- `UnyoCoreException(String message, Throwable cause)`
**Behavior:** Every public method in `AniyomiBridge` wraps failures in `UnyoCoreException` with the full Java stack trace in the message string. When catching via JNI, you get both the user-friendly message and the developer stack trace.
**Recommended Flutter handling:**
```dart
// Pseudocode — depends on your JNI bindings
try {
final results = bridge.getAnimeSearchResults(query, page, packageName);
} on JniException catch (e) {
// e.message contains both the summary and the full Java stack trace
log.error(e.message);
}
```
---
## 10. Breaking Changes from v1
If your Flutter code was written against the pre-refactor API (before multi-module split), these are the critical changes:
### 1. Extension keys changed from `extensionName` to `packageName`
**Before:**
```java
loadAnimeExtension(String extensionUrl, String basePath)
// Key was extracted from URL: "animepahe", "torrentio", etc.
unloadAnimeExtension(String extensionName, String version, String basePath)
// You passed extensionName + version, bridge constructed filename
```
**After:**
```java
loadAnimeExtension(String extensionUrl, String basePath, String packageName)
// You pass the full Android package name as key
unloadAnimeExtension(String packageName)
// Just packageName. Bridge tracks the JAR path internally.
```
**Migration:**
- Stop extracting extension names from URLs
- Use the Android package name (from the extension repo JSON) as the key everywhere
- Update your Flutter model to pass `packageName` to all methods
### 2. `getAnimeSearchResults` / `getMangaSearchResults` signature change
**Before:**
```java
getAnimeSearchResults(String query, int page, String animeSource)
// 'animeSource' was the extension name
```
**After:**
```java
getAnimeSearchResults(String query, int page, String packageName)
// 'packageName' is the full package name
```
**Migration:** Pass `packageName` instead of extension name.
### 3. Content fetching methods signature change
**Before:**
```java
getEpisodeList(SAnime anime, String animeSource)
getVideoList(SEpisode episode, String animeSource)
```
**After:**
```java
getEpisodeList(SAnime anime, String packageName)
getVideoList(SEpisode episode, String packageName)
```
**Migration:** Same as above — replace `animeSource` string with `packageName`.
### 4. Removal of `correctedNames` map
**Before:** The bridge internally mapped names like `"zoro" → "hianime"` and `"aniwatch" → "aniwatchtv"`.
**After:** No internal name correction. You must use the exact `packageName`.
**Migration:** Remove any logic in Flutter that relied on these aliases. Use package names directly from the repo metadata.
### 5. Unload methods no longer take `version` or `basePath`
**Before:**
```java
unloadAnimeExtension(String extensionName, String version, String basePath)
```
**After:**
```java
unloadAnimeExtension(String packageName)
```
**Migration:** Simpler — just pass `packageName`.
### 6. Preferences API is new
**Before:** No way to read/write extension preferences from Flutter.
**After:** Full `getAnimePreferences`, `setAnimePreference`, `setAnimePreferences` API.
**Migration:** If your app previously couldn't handle configurable sources, now you can/should expose these preferences in your UI.
### 7. `MediaSourceUtils` now uses per-source filters
**Before:**
```kotlin
animeSource.getSearchAnime(page, query, filters = defaultAnimeFilterList)
```
**After:**
```kotlin
animeSource.getSearchAnime(page, query, filters = animeSource.getFilterList())
```
**Impact:** Search results may differ if an extension had custom filters. This is generally more correct but could change behavior for extensions that rely on non-default filter states.
---
## 11. JNI Implementation Notes
### Class Loading
The fat JAR (`unyo-core-1.0.0.jar`) contains everything including the Android SDK stubs, Kotlin stdlib, and all dependencies. Load it with a standard `URLClassLoader` or from the classpath.
**Important:** The `Main.class` (`dev.k3vinb5.unyo.core.cli.Main`) is excluded from the fat JAR (consumed by external Flutter app). The class you instantiate is `dev.k3vinb5.unyo.core.AniyomiBridge`.
### Threading
All content-fetching methods use `kotlinx.coroutines.runBlocking` internally. They **will block the calling thread**. In Flutter, always call these methods through a background isolate or `compute()` / `Isolate.run()`.
The internal maps (`animeSources`, `mangaSources`, `classLoaders`) use `ConcurrentHashMap`, so `load*` and `unload*` are thread-safe. However, you should not load and unload the same extension concurrently.
### Object Lifetimes
`SAnime`, `SEpisode`, `SManga`, `SChapter` are `Serializable`. You can safely pass them across JNI boundaries and cache them in Dart if needed.
`Video` contains `okhttp3.Headers` which is **not serializable**. If you need to serialize video data (e.g., for Dart isolates or JSON), convert `Headers` to a `Map<String, List<String>>` or use the `SerializableVideo` Kotlin helper class.
### Memory
Each loaded extension gets its own `URLClassLoader`. When you call `unload*Extension`, the classloader is closed and the JAR file is deleted. This should release most memory, but some classes may remain in the JVM's metaspace until GC.
### Logging
The bridge uses JBoss Logging. Logs go to stdout/stderr by default. In a Flutter desktop app, you'll see them in the console. Consider redirecting `java.util.logging` or JBoss LogManager to your Dart logging system if you need structured logs.
---
## Appendix: Quick Reference — Method Signatures
```java
// Initialization
public static void init()
// Anime lifecycle
public void loadAnimeExtension(String extensionUrl, String basePath, String packageName)
public void unloadAnimeExtension(String packageName)
// Manga lifecycle
public void loadMangaExtension(String extensionUrl, String basePath, String packageName)
public void unloadMangaExtension(String packageName)
// Query loaded
public List<String> getAnimeLoadedExtensions()
public List<String> getMangaLoadedExtensions()
// Preferences (anime only, currently)
public List<PreferenceItem> getAnimePreferences(String packageName)
public void setAnimePreference(String packageName, String key, Object value)
public void setAnimePreferences(String packageName, Map<String, Object> values)
// Anime content
public List<SAnime> getAnimeSearchResults(String query, int page, String packageName)
public List<SEpisode> getEpisodeList(SAnime anime, String packageName)
public List<Video> getVideoList(SEpisode episode, String packageName)
// Manga content
public List<SManga> getMangaSearchResults(String query, int page, String packageName)
public List<SChapter> getChapterList(SManga manga, String packageName)
public List<Page> getPageList(SChapter chapter, String packageName)
```

View File

@@ -1,4 +1,4 @@
# AniyomiBridge Core
# Unyo Core
A JVM runtime for running Aniyomi (anime/manga streaming) Android extensions on desktop. This project bridges the gap between Android's framework and desktop Java/Kotlin by providing a compatibility layer that lets Aniyomi extensions execute outside of the Android ecosystem.
@@ -8,8 +8,8 @@ A JVM runtime for running Aniyomi (anime/manga streaming) Android extensions on
# Build the fat JAR (includes all dependencies)
mvn clean package
# Output: aniyomibridge-core/target/aniyomibridge-core-1.0.0.jar
# Main class: dev.k3vinb5.aniyomibridge.Main
# Output: aniyomi-bridge/target/aniyomiBridge-1.0.0.jar
# Main class: dev.k3vinb5.unyo.core.cli.Main
```
## Project Architecture
@@ -17,14 +17,14 @@ mvn clean package
This is a **multi-module Maven project** with two main modules:
```
aniyomibridge-core/
unyo-core/
├── pom.xml # Parent POM — coordinates modules
├── androidcompat/ # Android compatibility layer
│ ├── src/main/java/android/ # Custom Android stub implementations
│ ├── src/main/java/androidx/ # AndroidX stub implementations
│ ├── scripts/get-android.sh # Android SDK JAR download & patch script
│ └── pom.xml
├── aniyomibridge-core/ # Core bridge module
├── aniyomi-bridge/ # Core bridge module
│ ├── src/main/java/ # Java sources (bridge core)
│ ├── src/main/kotlin/ # Kotlin sources (Tachiyomi/Aniyomi interfaces)
│ └── pom.xml
@@ -41,7 +41,7 @@ Provides Android framework classes on desktop JVM using a **hybrid approach** in
**Result**: A merged Android API where ~4,000 SDK classes work as-is, and ~30+ critical classes have desktop-aware implementations.
### Module: `aniyomibridge-core`
### Module: `aniyomi-bridge`
The core bridge that loads and executes Aniyomi/Tachiyomi extensions.
@@ -67,16 +67,16 @@ Maven builds modules in dependency order:
1. **Parent POM** (coordinates children, does not produce a JAR)
2. **`androidcompat`** (no internal dependencies)
3. **`aniyomibridge-core`** (depends on `androidcompat`)
3. **`aniyomi-bridge`** (depends on `androidcompat`)
`androidcompat` **must** finish its `package` phase before `aniyomibridge-core` can even start compiling.
`androidcompat` **must** finish its `package` phase before `aniyomi-bridge` can even start compiling.
### Phase-by-Phase Breakdown
#### 1. `clean` phase
**Plugin:** `maven-clean-plugin`
- Deletes `androidcompat/target/` and `aniyomibridge-core/target/`
- Deletes `androidcompat/target/` and `aniyomi-bridge/target/`
- Fresh slate. Any previously downloaded `android.jar` or compiled classes are removed.
#### 2. `validate` through `process-sources`
@@ -141,10 +141,10 @@ Two different things happen per module:
- Packages `target/classes/` into `androidcompat/target/androidcompat-1.0.0.jar`
- This JAR contains the merged Android API (SDK + custom stubs)
##### In `aniyomibridge-core`:
##### In `aniyomi-bridge`:
**Plugins:** `maven-jar-plugin` → `maven-shade-plugin`
1. **maven-jar-plugin** creates the "thin" JAR: `aniyomibridge-core-1.0.0.jar`
1. **maven-jar-plugin** creates the "thin" JAR: `aniyomiBridge-1.0.0.jar`
2. **maven-shade-plugin** then runs:
- Takes the thin JAR
@@ -152,10 +152,10 @@ Two different things happen per module:
- **Unpacks** all other dependencies (OkHttp, Kotlin stdlib, Gson, etc.)
- Applies filter rules:
- `minimizeJar=true` strips unused classes from dependencies
- A `<filter>` for `androidcompat` forces **all** its classes to be included (otherwise they'd be stripped because extensions load them dynamically, not via direct references in core code)
- Excludes `Main.class` from the final JAR (consumed by Flutter externally)
- Excludes signing files (`*.SF`, `*.DSA`, `*.RSA`) and `module-info.class`
- Creates the **fat JAR**: `aniyomibridge-core/target/aniyomibridge-core-1.0.0.jar`
- A `<filter>` for `androidcompat` forces **all** its classes to be included (otherwise they'd be stripped because extensions load them dynamically, not via direct references in core code)
- Excludes `Main.class` from the final JAR (consumed by Flutter externally)
- Excludes signing files (`*.SF`, `*.DSA`, `*.RSA`) and `module-info.class`
- Creates the **fat JAR**: `aniyomi-bridge/target/aniyomiBridge-1.0.0.jar`
> **Result:** A single ~90MB JAR containing your core code + the entire merged Android API + all dependencies.
@@ -181,7 +181,7 @@ $ mvn clean package
package → maven-jar-plugin creates androidcompat-1.0.0.jar
↳ contains merged Android API
[MODULE: aniyomibridge-core]
[MODULE: aniyomi-bridge]
validate → check pom
...
compile → compile core sources (can reference androidcompat classes)
@@ -190,12 +190,12 @@ $ mvn clean package
→ maven-shade-plugin creates fat JAR
↳ unpacks androidcompat + all deps
↳ filters: include ALL androidcompat, exclude Main.class
↳ outputs aniyomibridge-core-1.0.0.jar (~90MB)
↳ outputs aniyomiBridge-1.0.0.jar (~90MB)
```
### Why This Order Matters
1. **`androidcompat` must build first** — `aniyomibridge-core` declares it as a dependency, so Maven enforces this order automatically.
1. **`androidcompat` must build first** — `aniyomi-bridge` declares it as a dependency, so Maven enforces this order automatically.
2. **`generate-resources` must happen before `process-resources`** — You can't unpack the JAR until it exists. The download script creates it during `generate-resources`, then Ant unpacks it during `process-resources`.
@@ -203,7 +203,7 @@ $ mvn clean package
4. **Custom stubs overwrite SDK classes in `target/classes/`** — This is by design. The compiler writes to the same directory that Ant unpacked into, so your `Context.java` overwrites the SDK's `Context.class`.
5. **`minimizeJar` + filter is critical** — Without the shade filter for `androidcompat`, Maven Shade would see that `aniyomibridge-core` never directly references `android.graphics.Bitmap` (extensions load it via reflection/ClassLoader), so it would strip it out. The filter forces inclusion of the entire merged Android API.
5. **`minimizeJar` + filter is critical** — Without the shade filter for `androidcompat`, Maven Shade would see that `aniyomi-bridge` never directly references `android.graphics.Bitmap` (extensions load it via reflection/ClassLoader), so it would strip it out. The filter forces inclusion of the entire merged Android API.
## Key Technologies
@@ -234,7 +234,7 @@ $ mvn clean package
| `generate-new-core-mac.sh` | Build + copy JAR to Flutter app assets (Mac path) |
| `androidcompat/scripts/get-android.sh` | Download and patch Android SDK JAR |
Both build scripts copy the output to a sibling project `k3vinb5_aniyomi_bridge/assets/`.
Both build scripts copy the output to a sibling project `unyo-lib/assets/`.
## IntelliJ IDEA Setup

View File

@@ -6,12 +6,12 @@
<parent>
<groupId>dev.k3vinb5</groupId>
<artifactId>aniyomibridge-parent</artifactId>
<artifactId>unyo-core</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>aniyomibridge-androidcompat</artifactId>
<name>AniyomiBridge Android Compatibility</name>
<artifactId>androidcompat</artifactId>
<name>Unyo Core Android Compatibility</name>
<description>Android compatibility layer for running Aniyomi extensions on desktop JVM</description>
<packaging>jar</packaging>

View File

@@ -6,19 +6,19 @@
<parent>
<groupId>dev.k3vinb5</groupId>
<artifactId>aniyomibridge-parent</artifactId>
<artifactId>unyo-core</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>aniyomibridge-core</artifactId>
<name>AniyomiBridge Core</name>
<artifactId>aniyomiBridge</artifactId>
<name>Aniyomi Bridge</name>
<description>Core bridge module for loading and executing Aniyomi extensions</description>
<dependencies>
<!-- Android Compatibility Layer -->
<dependency>
<groupId>dev.k3vinb5</groupId>
<artifactId>aniyomibridge-androidcompat</artifactId>
<artifactId>androidcompat</artifactId>
<version>${project.version}</version>
</dependency>
@@ -185,7 +185,7 @@
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>dev.k3vinb5.aniyomibridge.cli.Main</mainClass>
<mainClass>dev.k3vinb5.unyo.core.cli.Main</mainClass>
</transformer>
</transformers>
<filters>
@@ -206,11 +206,11 @@
<artifact>${project.groupId}:${project.artifactId}</artifact>
<excludes>
<!-- CLI test classes are excluded from the fat JAR -->
<exclude>dev/k3vinb5/aniyomibridge/cli/**</exclude>
<exclude>dev/k3vinb5/unyo/core/cli/**</exclude>
</excludes>
</filter>
<filter>
<artifact>dev.k3vinb5:aniyomibridge-androidcompat</artifact>
<artifact>dev.k3vinb5:androidcompat</artifact>
<includes>
<include>**</include>
</includes>

View File

@@ -1,13 +1,13 @@
package dev.k3vinb5.aniyomibridge;
package dev.k3vinb5.unyo.core;
import android.app.Application;
import androidx.preference.PreferenceScreen;
import dev.k3vinb5.aniyomibridge.exceptions.UnyoCoreException;
import dev.k3vinb5.aniyomibridge.loader.ExtensionLoader;
import dev.k3vinb5.aniyomibridge.loader.ExtensionPatches;
import dev.k3vinb5.aniyomibridge.loader.JarInspector;
import dev.k3vinb5.aniyomibridge.utils.ApkUtils;
import dev.k3vinb5.aniyomibridge.utils.MediaSourceUtils;
import dev.k3vinb5.unyo.core.exceptions.UnyoCoreException;
import dev.k3vinb5.unyo.core.loader.ExtensionLoader;
import dev.k3vinb5.unyo.core.loader.ExtensionPatches;
import dev.k3vinb5.unyo.core.loader.JarInspector;
import dev.k3vinb5.unyo.core.utils.ApkUtils;
import dev.k3vinb5.unyo.core.utils.MediaSourceUtils;
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource;
import eu.kanade.tachiyomi.animesource.model.SAnime;
import eu.kanade.tachiyomi.animesource.model.SEpisode;
@@ -46,7 +46,8 @@ public class AniyomiBridge {
private static final Map<String, AnimeHttpSource> animeSources = new ConcurrentHashMap<>();
private static final Map<String, CatalogueSource> mangaSources = new ConcurrentHashMap<>();
public static final Map<String, ClassLoader> classLoaders = new ConcurrentHashMap<>();
private static final Map<String, String> extensionJarPaths = new ConcurrentHashMap<>();
private static final Map<String, String> animeExtensionJarPaths = new ConcurrentHashMap<>();
private static final Map<String, String> mangaExtensionJarPaths = new ConcurrentHashMap<>();
private static String formatWithStackTrace(String prefix, Throwable e) {
StringWriter sw = new StringWriter();
@@ -65,7 +66,7 @@ public class AniyomiBridge {
File jarFile = ApkUtils.dex2Jar(extensionUrl, getExtensionFileName(extensionUrl), basePath);
try {
assert jarFile != null;
extensionJarPaths.put(packageName, jarFile.getPath());
animeExtensionJarPaths.put(packageName, jarFile.getPath());
String className = JarInspector.findMainClass(jarFile.getPath(), AnimeHttpSource.class);
if (className == null || className.isEmpty()) {
className = JarInspector.getClassName(jarFile.getPath(), extensionName);
@@ -100,7 +101,7 @@ public class AniyomiBridge {
LOGGER.warnf("Failed to close classloader for %s: %s", packageName, ioe.getMessage());
}
}
String jarPath = extensionJarPaths.get(packageName);
String jarPath = animeExtensionJarPaths.get(packageName);
if (jarPath != null) {
File jarFile = new File(jarPath);
if (jarFile.exists()) {
@@ -109,7 +110,7 @@ public class AniyomiBridge {
LOGGER.warnf("Failed to delete JAR file: %s", jarPath);
}
}
extensionJarPaths.remove(packageName);
animeExtensionJarPaths.remove(packageName);
}
animeSources.remove(packageName);
classLoaders.remove(packageName);
@@ -125,7 +126,7 @@ public class AniyomiBridge {
File jarFile = ApkUtils.dex2Jar(extensionUrl, getExtensionFileName(extensionUrl), basePath);
try {
assert jarFile != null;
extensionJarPaths.put(packageName, jarFile.getPath());
mangaExtensionJarPaths.put(packageName, jarFile.getPath());
String className = JarInspector.findMainClass(jarFile.getPath(), CatalogueSource.class);
if (className == null || className.isEmpty()) {
className = JarInspector.getClassName(jarFile.getPath(), extensionName);
@@ -160,7 +161,7 @@ public class AniyomiBridge {
LOGGER.warnf("Failed to close classloader for %s: %s", packageName, ioe.getMessage());
}
}
String jarPath = extensionJarPaths.get(packageName);
String jarPath = mangaExtensionJarPaths.get(packageName);
if (jarPath != null) {
File jarFile = new File(jarPath);
if (jarFile.exists()) {
@@ -169,7 +170,7 @@ public class AniyomiBridge {
LOGGER.warnf("Failed to delete JAR file: %s", jarPath);
}
}
extensionJarPaths.remove(packageName);
mangaExtensionJarPaths.remove(packageName);
}
mangaSources.remove(packageName);
classLoaders.remove(packageName);
@@ -188,6 +189,16 @@ public class AniyomiBridge {
return mangaSources.keySet().stream().toList();
}
// ==================== INSTALLED EXTENSIONS API ====================
public Map<String, String> getInstalledAnimeExtensions() {
return new HashMap<>(animeExtensionJarPaths);
}
public Map<String, String> getInstalledMangaExtensions() {
return new HashMap<>(mangaExtensionJarPaths);
}
// ==================== PREFERENCES API ====================
public List<PreferenceItem> getAnimePreferences(String packageName) {

View File

@@ -1,4 +1,4 @@
package dev.k3vinb5.aniyomibridge;
package dev.k3vinb5.unyo.core;
import java.util.List;

View File

@@ -1,4 +1,4 @@
package dev.k3vinb5.aniyomibridge.cli;
package dev.k3vinb5.unyo.core.cli;
import java.util.List;
import java.util.Map;

View File

@@ -1,8 +1,8 @@
package dev.k3vinb5.aniyomibridge.cli;
package dev.k3vinb5.unyo.core.cli;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import dev.k3vinb5.aniyomibridge.AniyomiBridge;
import dev.k3vinb5.unyo.core.AniyomiBridge;
import eu.kanade.tachiyomi.animesource.model.SAnime;
import eu.kanade.tachiyomi.animesource.model.SEpisode;
import eu.kanade.tachiyomi.animesource.model.Video;
@@ -26,7 +26,7 @@ import java.util.stream.Collectors;
* Batch tests all anime extensions from configured repos.
*
* Usage:
* java -cp ... dev.k3vinb5.aniyomibridge.cli.ExtensionBatchTester [options]
* java -cp ... dev.k3vinb5.unyo.core.cli.ExtensionBatchTester [options]
*
* Options:
* --concurrency N Thread pool size (default: CPU cores)

View File

@@ -1,11 +1,11 @@
package dev.k3vinb5.aniyomibridge.cli;
package dev.k3vinb5.unyo.core.cli;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import dev.k3vinb5.aniyomibridge.AniyomiBridge;
import dev.k3vinb5.aniyomibridge.loader.ExtensionLoader;
import dev.k3vinb5.aniyomibridge.loader.JarInspector;
import dev.k3vinb5.aniyomibridge.utils.ApkUtils;
import dev.k3vinb5.unyo.core.AniyomiBridge;
import dev.k3vinb5.unyo.core.loader.ExtensionLoader;
import dev.k3vinb5.unyo.core.loader.JarInspector;
import dev.k3vinb5.unyo.core.utils.ApkUtils;
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource;
import okhttp3.OkHttpClient;
import okhttp3.Request;
@@ -23,7 +23,7 @@ import java.util.List;
* 2. torrentioanime - known broken, getClassName fails, findMainClass should fix
*
* Usage:
* java -cp ... dev.k3vinb5.aniyomibridge.cli.JarInspectorTest
* java -cp ... dev.k3vinb5.unyo.core.cli.JarInspectorTest
*/
public class JarInspectorTest {

View File

@@ -1,9 +1,9 @@
package dev.k3vinb5.aniyomibridge.cli;
package dev.k3vinb5.unyo.core.cli;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import dev.k3vinb5.aniyomibridge.AniyomiBridge;
import dev.k3vinb5.aniyomibridge.PreferenceItem;
import dev.k3vinb5.unyo.core.AniyomiBridge;
import dev.k3vinb5.unyo.core.PreferenceItem;
import eu.kanade.tachiyomi.animesource.model.SAnime;
import eu.kanade.tachiyomi.animesource.model.SEpisode;
import eu.kanade.tachiyomi.animesource.model.Video;

View File

@@ -1,4 +1,4 @@
package dev.k3vinb5.aniyomibridge.cli;
package dev.k3vinb5.unyo.core.cli;
/**
* Configuration for an extension repository.

View File

@@ -1,4 +1,4 @@
package dev.k3vinb5.aniyomibridge.cli;
package dev.k3vinb5.unyo.core.cli;
import java.util.List;

View File

@@ -1,4 +1,4 @@
package dev.k3vinb5.aniyomibridge.cli;
package dev.k3vinb5.unyo.core.cli;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

View File

@@ -1,4 +1,4 @@
package dev.k3vinb5.aniyomibridge.cli;
package dev.k3vinb5.unyo.core.cli;
/**
* Gson model for a source within an extension entry.

View File

@@ -1,4 +1,4 @@
package dev.k3vinb5.aniyomibridge.exceptions;
package dev.k3vinb5.unyo.core.exceptions;
public class UnyoCoreException extends RuntimeException {
public UnyoCoreException(String message) {

View File

@@ -1,6 +1,6 @@
package dev.k3vinb5.aniyomibridge.loader;
package dev.k3vinb5.unyo.core.loader;
import dev.k3vinb5.aniyomibridge.AniyomiBridge;
import dev.k3vinb5.unyo.core.AniyomiBridge;
import kotlin.Pair;
import org.jboss.logging.Logger;

View File

@@ -1,6 +1,6 @@
package dev.k3vinb5.aniyomibridge.loader;
package dev.k3vinb5.unyo.core.loader;
import dev.k3vinb5.aniyomibridge.loader.interceptors.TorrentioAnimeInteceptor;
import dev.k3vinb5.unyo.core.loader.interceptors.TorrentioAnimeInteceptor;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;

View File

@@ -1,4 +1,4 @@
package dev.k3vinb5.aniyomibridge.loader;
package dev.k3vinb5.unyo.core.loader;
import org.jboss.logging.Logger;

View File

@@ -1,4 +1,4 @@
package dev.k3vinb5.aniyomibridge.loader;
package dev.k3vinb5.unyo.core.loader;
import org.jboss.logging.Logger;

View File

@@ -1,4 +1,4 @@
package dev.k3vinb5.aniyomibridge.loader.interceptors;
package dev.k3vinb5.unyo.core.loader.interceptors;
import com.google.gson.Gson;
import eu.kanade.tachiyomi.network.RequestsKt;

View File

@@ -1,4 +1,4 @@
package dev.k3vinb5.aniyomibridge.utils;
package dev.k3vinb5.unyo.core.utils;
import com.googlecode.d2j.Method;
import com.googlecode.d2j.dex.Dex2jar;

View File

@@ -1,4 +1,4 @@
package dev.k3vinb5.aniyomibridge.utils
package dev.k3vinb5.unyo.core.utils
import eu.kanade.tachiyomi.animesource.model.*
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource

View File

@@ -22,9 +22,9 @@ class NetworkHelper(
private val clientBuilder: OkHttpClient.Builder = run {
val builder = OkHttpClient.Builder()
.cookieJar(cookieJar)
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.callTimeout(5, TimeUnit.MINUTES)
.connectTimeout(120, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.callTimeout(120, TimeUnit.SECONDS)
.dispatcher(Dispatcher().apply { maxRequestsPerHost = 10 })
.cache(
Cache(

Some files were not shown because too many files have changed in this diff Show More