- * ---
- * a ActivityAdapter requires you to implement both a {@link ActivityAdapterService} that you can register with Tenshi
- * and a {@link ActivityAdapterActivity} that contains the main logic of your content adapter.
- * Communication and callbacks are handled automatically between activity, service and Tenshi
- */
-public abstract class ActivityAdapterActivity
- * loads:
- * {@link #malID}
- * {@link #enTitle}
- * {@link #jpTitle}
- * {@link #episode}
- * {@link #persistentStorage}
- *
- * @return was the load successful? if false, do not continue
- */
- protected boolean loadAdapterParams() {
- // ensure we have a intent
- final Intent i = getIntent();
- if (i == null)
- return false;
-
- // load data
- malID = i.getIntExtra(EXTRA_MAL_ID, -1);
- enTitle = i.getStringExtra(EXTRA_ANIME_TITLE_EN);
- jpTitle = i.getStringExtra(EXTRA_ANIME_TITLE_JP);
- episode = i.getIntExtra(EXTRA_TARGET_EPISODE, -1);
- persistentStorage = i.getStringExtra(EXTRA_PERSISTENT_STORAGE);
-
- // persistent storage is optional, but cannot be null
- if (persistentStorage == null)
- persistentStorage = "";
-
- return true;
- }
-
- /**
- * invoke the callback of the content adapter, with automatic persistent storage
- * after this, the activity should call finish()
- *
- * @param streamUrl the stream url for the callback
- */
- protected void invokeCallback(@Nullable String streamUrl) {
- invokeCallback(streamUrl, persistentStorage == null ? "" : persistentStorage);
- }
-
- /**
- * invoke the callback of the content adapter
- * after this, the activity should call finish()
- *
- * @param streamUrl the stream url for the callback
- * @param persStorage persistent storage for the callback
- */
- protected void invokeCallback(@Nullable String streamUrl, @NonNull String persStorage) {
- final Intent i = new Intent(this, getServiceClass());
- i.setAction(ActivityAdapterService.ACTION_NOTIFY_RESULT);
- i.putExtra(ActivityAdapterService.EXTRA_RESULT_STREAM_URL, streamUrl);
- i.putExtra(ActivityAdapterService.EXTRA_RESULT_PERSISTENT_STORAGE, persStorage);
- startService(i);
- }
-
- /**
- * get the activity adapter service to invoke the callback on
- *
- * @return the service class
- */
- @NonNull
- protected abstract Class
- * ---
- * a ActivityAdapter requires you to implement both a {@link ActivityAdapterService} that you can register with Tenshi
- * and a {@link ActivityAdapterActivity} that contains the main logic of your content adapter.
- * Communication and callbacks are handled automatically between activity, service and Tenshi
- */
-public abstract class ActivityAdapterService>() {
+ @Override
+ @EverythingIsNonNull
+ public void onResponse(Call
> call, Response
> response) {
+ synchronized (DEFINITIONS_LOCK) {
+ // check the call was successful
+ if (response.isSuccessful()) {
+ // response is ok, set definitions from body
+ adapterDefinitions = response.body();
+ } else {
+ //failed
+ adapterDefinitions = new ArrayList<>();
+ }
+ DEFINITIONS_LOCK.notifyAll();
+ }
+ }
+
+ @Override
+ @EverythingIsNonNull
+ public void onFailure(Call
> call, Throwable t) {
+ synchronized (DEFINITIONS_LOCK) {
+ adapterDefinitions = new ArrayList<>();
+ DEFINITIONS_LOCK.notifyAll();
+ }
+ }
+ });
+ }
+
+ /**
+ * load definitions from R.raw
+ *
+ * @param ctx the context to load from
+ */
+ private static void loadDefinitionRaw(@NonNull Context ctx) {
+ synchronized (DEFINITIONS_LOCK) {
+ try {
+ // open reader for the resource
+ try (BufferedReader rawIn = new BufferedReader(new InputStreamReader(
+ ctx.getResources().openRawResource(R.raw.debug_definition)))) {
+
+ // read and append line- by- line
+ final StringBuilder json = new StringBuilder();
+ String ln;
+ while ((ln = rawIn.readLine()) != null)
+ json.append(ln).append("\n");
+
+ // deserialize from json
+
+ adapterDefinitions = new Gson()
+ .fromJson(json.toString(), new TypeToken
>() {
+ }.getType());
+
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ adapterDefinitions = new ArrayList<>();
+ }
+ DEFINITIONS_LOCK.notifyAll();
+ }
+ }
+
+ /**
+ * build the unique name for a definition
+ *
+ * @param def the definition
+ * @return the unique name
+ */
+ public static String buildUniqueName(@NonNull WebAdapterDefinition def) {
+ return Constants.UNIQUE_NAME_PREFIX + def.name;
+ }
+}
diff --git a/app/src/main/java/io/github/shadow578/tenshicontent/webadapter/definition/WebAdapterRetrofit.java b/app/src/main/java/io/github/shadow578/tenshicontent/webadapter/definition/WebAdapterRetrofit.java
new file mode 100644
index 0000000..3520dc0
--- /dev/null
+++ b/app/src/main/java/io/github/shadow578/tenshicontent/webadapter/definition/WebAdapterRetrofit.java
@@ -0,0 +1,22 @@
+package io.github.shadow578.tenshicontent.webadapter.definition;
+
+import java.util.List;
+
+import io.github.shadow578.tenshicontent.webadapter.definition.model.WebAdapterDefinition;
+import retrofit2.Call;
+import retrofit2.http.GET;
+import retrofit2.http.Url;
+
+/**
+ * retrofit interface for web adapters
+ */
+public interface WebAdapterRetrofit {
+
+ /**
+ * load web adapter definitions from a json file
+ * @param jsonUrl the json file to load
+ * @return the adapter definitions
+ */
+ @GET
+ Call
> getDefinitions(@Url String jsonUrl);
+}
diff --git a/app/src/main/java/io/github/shadow578/tenshicontent/webadapter/definition/model/WebAdapterDefinition.java b/app/src/main/java/io/github/shadow578/tenshicontent/webadapter/definition/model/WebAdapterDefinition.java
new file mode 100644
index 0000000..f420342
--- /dev/null
+++ b/app/src/main/java/io/github/shadow578/tenshicontent/webadapter/definition/model/WebAdapterDefinition.java
@@ -0,0 +1,65 @@
+package io.github.shadow578.tenshicontent.webadapter.definition.model;
+
+import androidx.annotation.Nullable;
+
+/**
+ * definition of a web adapter loaded from a json file
+ */
+@SuppressWarnings({"unused", "RedundantSuppression"})
+public final class WebAdapterDefinition {
+
+ /**
+ * unique name for this adapter
+ */
+ public String name;
+
+ /**
+ * display name of this adapter
+ */
+ public String displayName;
+
+ /**
+ * regex pattern to validate the storage.
+ * may be null to disable verification
+ */
+ @Nullable
+ public String storagePattern;
+
+ /**
+ * search url for this adapter,
+ * formatted: %s for escaped query string
+ */
+ public String searchUrl;
+
+ /**
+ * episode url of this adapter,
+ * formatted: %s for persistent storage content, %d for target episode
+ */
+ public String episodeUrl;
+
+ /**
+ * url to the payload, injected as script.
+ */
+ public String payload;
+
+ /**
+ * string to use as the user agent.
+ * if null ignore this
+ */
+ @Nullable
+ public String userAgentOverride;
+
+ /**
+ * is webview dom storage reqiured to be enabled?
+ * if null ignore this
+ */
+ @Nullable
+ public Boolean domStorageEnabled;
+
+ /**
+ * allow content access in the webview.
+ * if null ignore this
+ */
+ @Nullable
+ public Boolean allowContentAccess;
+}
diff --git a/app/src/main/res/raw/debug_definition.json b/app/src/main/res/raw/debug_definition.json
new file mode 100644
index 0000000..f6d2009
--- /dev/null
+++ b/app/src/main/res/raw/debug_definition.json
@@ -0,0 +1,13 @@
+[
+ {
+ "name": "genoanime.web",
+ "displayName": "GenoAnime",
+ "storagePattern": null,
+ "searchUrl": "https://genoanime.com/search?xquery=%s",
+ "episodeUrl": "https://genoanime.com/watch?name=%s&episode=%d",
+ "payload": "https://raw.githubusercontent.com/Tenshiorg/Tenshi-Content/kohai/webadapters/payloads/genoanime.js",
+ "userAgentOverride": null,
+ "domStorageEnabled": null,
+ "allowContentAccess": null
+ }
+]
\ No newline at end of file
diff --git a/app/src/main/res/raw/debug_payload.js b/app/src/main/res/raw/debug_payload.js
new file mode 100644
index 0000000..12b5029
--- /dev/null
+++ b/app/src/main/res/raw/debug_payload.js
@@ -0,0 +1,142 @@
+/**
+ * Tenshi payload for genoanime.com
+ * for use with the WebAdapter (Activity)
+ *
+ * also adds search query support on /search using xquery path parameter
+ */
+function __tenshi_payload_init() {
+
+ // check window location is geno
+ var windowLoc = window.location;
+ if (windowLoc.host === 'genoanime.com') {
+ // parse query parameters
+ var queryParams = new URLSearchParams(windowLoc.search);
+
+ // if we are on the search page
+ if (windowLoc.pathname === '/search') {
+ // get query parameter 'xquery'
+ if (queryParams.has('xquery')) {
+ // do the search after a few ms
+ var query = queryParams.get('xquery');
+ setTimeout(() => {
+ __perform_search(query);
+ }, 750);
+ }
+ }
+
+ // we are on the episode watch page
+ if (windowLoc.pathname === '/watch') {
+ // parse query parameters
+ // geno is so nice to actually just use a query parameter 'name' for their anime pages
+ // so we can just kinda take it
+ if (queryParams.has('name')) {
+ var animeName = queryParams.get('name');
+ App.log('found anime name= ' + animeName);
+ Tenshi.setPersistentStorage(animeName);
+ }
+
+ // do video stuff on geno
+ __video_switcharoo_geno();
+ }
+
+ // we are "in" the video iframe
+ if (windowLoc.pathname.startsWith('/player')) {
+ __video_switcharoo_frame();
+ }
+ } else {
+ // we are no longer on geno
+ __video_switcharoo_frame();
+ }
+}
+
+/**
+ * perform a anime search
+ * @param {String} query the search query
+ */
+function __perform_search(query) {
+ // set value of search bar
+ var searchBar = document.getElementById('search-anime');
+ searchBar.value = query;
+
+ // manually call search function
+ searchAnime();
+}
+
+/**
+ * does the video stuff
+ *
+ * we replace the video iframe with a fixed image.
+ * on click, we load into the iframe and grab the url of the video
+ * this has to be done like that because the iframe is a different origin (so 'normal' js injection does not work)
+ */
+function __video_switcharoo_geno() {
+ // get container div and iframe elements
+ var frameContainer = document.getElementById('video');
+ var frame = frameContainer.querySelector('iframe');
+
+ // save the iframe location for later
+ const frameSrc = frame.src;
+
+ // TODO we could also just directly load into the iframe
+ // maybe this will be a config option in the future?
+ //if (directStartPlayback) {
+ // window.location = frameSrc;
+ //}
+
+ // yeet the frame
+ frame.remove();
+
+ // modify the frame container to have a background image
+ frameContainer.style.backgroundColor = '#000000';
+ frameContainer.style.backgroundImage = 'url(\'https://storage.googleapis.com/genoanime/genologo.jpg\')';
+ frameContainer.style.backgroundSize = '100% 100%';
+
+ // add text into the container
+ frameContainer.innerHTML = `
Click here to Play
`;
+
+ // set onclick of the container div
+ frameContainer.onclick = function () {
+ window.location = frameSrc;
+ };
+}
+
+/**
+ * does the video stuff
+ *
+ * we are now "in" the iframe with the video, and quickly grab the video url
+ */
+function __video_switcharoo_frame() {
+ // notify the user
+ App.toast('Playback will start shortly...');
+
+ // do this every 500ms until we finish
+ // geno seems to sometimes clear the onplay event on the video
+ var timerId = setInterval(() => {
+ // find all videos and setup listener
+ var allVideos = document.querySelectorAll('video');
+ allVideos.forEach(video => {
+ App.log('setup for video id: ' + video.id);
+
+ // pause the video
+ // this improves reliability
+ video.pause();
+
+ // setup onplay event to capture url
+ video.onplay = function () {
+ // stop the video
+ video.pause();
+
+ // get the video url and finish
+ var vidUrl = video.currentSrc.toString();
+ if (vidUrl !== '') {
+ clearInterval(timerId);
+ Tenshi.finish(vidUrl);
+ }
+ };
+
+ // enable autoplay and start the video
+ video.autoplay = true;
+ video.play();
+ });
+ }, 500);
+}
\ No newline at end of file
diff --git a/app/src/main/res/raw/fouranime_payload.js b/app/src/main/res/raw/fouranime_payload.js
deleted file mode 100644
index aabc587..0000000
--- a/app/src/main/res/raw/fouranime_payload.js
+++ /dev/null
@@ -1,49 +0,0 @@
-// Tenshi JS payload for 4anime.to
-// injected after every page load using default injector
-// -- constants --
-const SLUG_REGEX = /(?:4anime.to\/)(.+)(?:-episode-\d+)(?:\?id=)?/;
-const VIDEO_POSTER = '';
-
-// -- update slug in persistent storage --
-var windowLoc = window.location.href;
-var slugMatch = SLUG_REGEX.exec(windowLoc);
-if (slugMatch != null) {
- var slug = slugMatch[1];
- App.log('SLUG_REGEX match for location ' + windowLoc + ' is: ' + slug);
- if (slug !== '') {
- Tenshi.setSlug(slug);
- }
-}
-
-// -- setup all videos on the page for capturing --
-var allVideos = document.querySelectorAll('video');
-for (var i = 0; i < allVideos.length; i++) {
- var video = allVideos[i];
- App.log('setup for video with id: ' + video.id);
-
- // disable autoplay on the video
- video.autoplay = false;
-
- // setup onplay event to capture the url
- video.onplay = function () {
- // stop the video and force show poster
- video.pause();
- video.setAttribute('poster', VIDEO_POSTER);
- video.autoplay = false;
- video.load();
-
- // get the url from the video and notify tenshi
- var vidUrl = video.currentSrc.toString();
- if (vidUrl !== '') {
- Tenshi.onStreamUrlFound(vidUrl);
- }
- };
-}
-
-// if we setup a video, notify the user they can start it now
-if (allVideos.length > 0) {
- App.toast('Start the Video now.');
-}
-
-// notify user we are successfully injected
-App.toast('Injected!');
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4fa10d4..3aaae2e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,6 +1,6 @@
Click here to Play
`;
+
+ // set onclick of the container div
+ frameContainer.onclick = function () {
+ window.location = frameSrc;
+ };
+}
+
+/**
+ * does the video stuff
+ *
+ * we are now "in" the iframe with the video, and quickly grab the video url
+ */
+function __video_switcharoo_frame() {
+ // notify the user
+ App.toast('Playback will start shortly...');
+
+ // do this every 500ms until we finish
+ // geno seems to sometimes clear the onplay event on the video
+ var timerId = setInterval(() => {
+ // find all videos and setup listener
+ var allVideos = document.querySelectorAll('video');
+ allVideos.forEach(video => {
+ App.log('setup for video id: ' + video.id);
+
+ // pause the video
+ // this improves reliability
+ video.pause();
+
+ // setup onplay event to capture url
+ video.onplay = function () {
+ // stop the video
+ video.pause();
+
+ // get the video url and finish
+ var vidUrl = video.currentSrc.toString();
+ if (vidUrl !== '') {
+ clearInterval(timerId);
+ Tenshi.finish(vidUrl);
+ }
+ };
+
+ // enable autoplay and start the video
+ video.autoplay = true;
+ video.play();
+ });
+ }, 500);
+}
\ No newline at end of file
diff --git a/webadapters/payloads/yugenanime.js b/webadapters/payloads/yugenanime.js
new file mode 100644
index 0000000..9b61dc9
--- /dev/null
+++ b/webadapters/payloads/yugenanime.js
@@ -0,0 +1,75 @@
+/**
+ * Tenshi payload for yugenani.me
+ * for use with the WebAdapter (Activity)
+ */
+function __tenshi_payload_init() {
+ // start removing ads
+ __ad_removal();
+
+ // check if we are on a episode page
+ const EPISODE_URL_REGEX = /(?:yugenani.me\/watch\/)(\d+\/.+)(?:\/\d+)/;
+ var locMatch = EPISODE_URL_REGEX.exec(window.location.href);
+
+ // if we are not, exit function
+ if (locMatch == null) {
+ return;
+ }
+
+ // otherwise, get slug from capture group and save it
+ var slug = locMatch[1];
+ if (slug !== '') {
+ App.log('found slug for location ' + window.location.href + ' : ' + slug);
+ Tenshi.setPersistentStorage(slug);
+ }
+
+ // get the main embed
+ var mainEmbed = document.getElementById('main-embed');
+ if (mainEmbed != null) {
+ var timerId = setInterval(() => {
+ // run query selector in the iframe (same origin)
+ var allVideos = mainEmbed.contentWindow.document.body.querySelectorAll('video');
+ allVideos.forEach(video => {
+ // disable autoplay on the video
+ App.log('setup for video with id: ' + video.id);
+ video.autoplay = false;
+ video.pause();
+
+ // grab the url from the video
+ // we don't have to care about waiting for onplay as the page doesn't have episode selection anyways
+ var vidUrl = video.currentSrc.toString();
+ if (vidUrl !== '') {
+ // stop timer
+ clearInterval(timerId);
+
+ // finish
+ Tenshi.finish(vidUrl);
+ }
+ });
+ }, 500);
+
+ // notify user
+ App.toast('Playback should start shortly...');
+ }
+}
+
+/**
+ * removes all iframes that are not inside the tag
+ */
+function __ad_removal() {
+ // check we are on the right page
+ if (window.location.host !== 'yugenani.me') {
+ return;
+ }
+
+ // ad remover
+ setInterval(() => {
+ // query all iframes on the page
+ var allIFrames = document.querySelectorAll('iframe');
+ allIFrames.forEach(iframe => {
+ // is this iframe outside the body tag?
+ if (!document.body.contains(iframe)) {
+ iframe.remove();
+ }
+ });
+ }, 100);
+}
\ No newline at end of file