sendErrorResult = message -> {
+ if (!sentResult[0]) {
+ sentResult[0] = true;
+ stopRouteScan(scan, null);
+ callback.onError(message);
+ }
+ return null;
+ };
+
+ listenForConnection(new ConnectionCallback() {
+ @Override
+ public void onJoin(JSONObject jsonSession) {
+ sentResult[0] = true;
+ stopRouteScan(scan, null);
+ callback.onJoin(jsonSession);
+ }
+
+ @Override
+ public boolean onSessionStartFailed(int errorCode) {
+ if (errorCode == 7 || errorCode == 15) {
+ // It network or timeout error retry
+ retry.run();
+ return false;
+ } else {
+ sendErrorResult.apply(ChromecastUtilities.createError("session_error",
+ "Failed to start session with error code: " + errorCode));
+ return true;
+ }
+ }
+
+ @Override
+ public boolean onSessionEndedBeforeStart(int errorCode) {
+ if (retries[0] < 10) {
+ retries[0]++;
+ retry.run();
+ return false;
+ } else {
+ sendErrorResult.apply(ChromecastUtilities.createError("session_error",
+ "Failed to to join existing route (" + routeId + ") " + retries[0] + 1 + " times before giving up."));
+ return true;
+ }
+ }
+ });
+
+ startRouteScan(15000L, scan, () ->
+ sendErrorResult.apply(ChromecastUtilities.createError("timeout", "Failed to join route (" + routeId + ") after 15s and " + (retries[0] + 1) + " tries."))
+ );
+ });
+ }
+
+ /**
+ * Will do one of two things:
+ *
+ * If no current connection will:
+ * 1)
+ * Displays the built in native prompt to the user.
+ * It will actively scan for routes and display them to the user.
+ * Upon selection it will immediately attempt to selectRoute the route.
+ * Will call onJoin, onError or onCancel, of callback.
+ *
+ * Else we have a connection, so:
+ * 2)
+ * Displays the active connection dialog which includes the option
+ * to disconnect.
+ * Will only call onCancel of callback if the user cancels the dialog.
+ *
+ * @param callback calls callback.success when we have joined a session,
+ * or callback.error if an error occurred or if the dialog was dismissed
+ */
+ public void requestSession(RequestSessionCallback callback) {
+ activity.runOnUiThread(() -> {
+ CastSession session = getSession();
+ if (session == null) {
+ // show the "choose a connection" dialog
+
+ // Add the connection listener callback
+ listenForConnection(callback);
+
+ // Create the dialog
+ // TODO accept theme as a config.xml option
+ MediaRouteChooserDialog builder = new MediaRouteChooserDialog(activity, R.style.AppTheme);
+ builder.setRouteSelector(new MediaRouteSelector.Builder()
+ .addControlCategory(CastMediaControlIntent.categoryForCast(appId))
+ .build());
+ builder.setCanceledOnTouchOutside(true);
+ builder.setOnCancelListener(dialog -> {
+ getSessionManager().removeSessionManagerListener(newConnectionListener, CastSession.class);
+ callback.onCancel();
+ });
+ builder.show();
+ } else {
+ // We are are already connected, so show the "connection options" Dialog
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ if (session.getCastDevice() != null) {
+ builder.setTitle(session.getCastDevice().getFriendlyName());
+ }
+ builder.setOnDismissListener(dialog -> callback.onCancel());
+ builder.setPositiveButton("Stop Casting", (dialog, which) ->
+ endSession(true, null)
+ );
+ builder.show();
+ }
+ });
+ }
+
+ /**
+ * Must be called from the main thread.
+ *
+ * @param callback calls callback.success when we have joined, or callback.error if an error occurred
+ */
+ private void listenForConnection(ConnectionCallback callback) {
+ // We should only ever have one of these listeners active at a time, so remove previous
+ getSessionManager().removeSessionManagerListener(newConnectionListener, CastSession.class);
+ newConnectionListener = new SessionListener() {
+ @Override
+ public void onSessionStarted(@NonNull CastSession castSession, @NonNull String sessionId) {
+ getSessionManager().removeSessionManagerListener(this, CastSession.class);
+ media.setSession(castSession);
+ callback.onJoin(ChromecastUtilities.createSessionObject(castSession));
+ }
+
+ @Override
+ public void onSessionStartFailed(@NonNull CastSession castSession, int errCode) {
+ if (callback.onSessionStartFailed(errCode)) {
+ getSessionManager().removeSessionManagerListener(this, CastSession.class);
+ }
+ }
+
+ @Override
+ public void onSessionEnded(@NonNull CastSession castSession, int errCode) {
+ if (callback.onSessionEndedBeforeStart(errCode)) {
+ getSessionManager().removeSessionManagerListener(this, CastSession.class);
+ }
+ }
+ };
+ getSessionManager().addSessionManagerListener(newConnectionListener, CastSession.class);
+ }
+
+ /**
+ * Starts listening for receiver updates.
+ * Must call stopRouteScan(callback) or the battery will drain with non-stop active scanning.
+ *
+ * @param timeout ms until the scan automatically stops,
+ * if 0 only calls callback.onRouteUpdate once with the currently known routes
+ * if null, will scan until stopRouteScan is called
+ * @param callback the callback to receive route updates on
+ * @param onTimeout called when the timeout hits
+ */
+ public void startRouteScan(Long timeout, ScanCallback callback, Runnable onTimeout) {
+ if (activity == null) return;
+
+ // Add the callback in active scan mode
+ activity.runOnUiThread(() -> {
+ MediaRouter mediaRouter = getMediaRouter();
+ if (mediaRouter == null) return;
+
+ callback.setMediaRouter(mediaRouter);
+
+ if (timeout != null && timeout == 0) {
+ // Send out the one time routes
+ callback.onFilteredRouteUpdate();
+ return;
+ }
+
+ // Add the callback in active scan mode
+ mediaRouter.addCallback(new MediaRouteSelector.Builder()
+ .addControlCategory(CastMediaControlIntent.categoryForCast(appId))
+ .build(),
+ callback,
+ MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN
+ );
+
+ // Send out the initial routes after the callback has been added.
+ // This is important because if the callback calls stopRouteScan only once, and it
+ // happens during this call of "onFilterRouteUpdate", there must actually be an
+ // added callback to remove to stop the scan.
+ callback.onFilteredRouteUpdate();
+
+ if (timeout != null) {
+ // remove the callback after timeout ms, and notify caller
+ handler.postDelayed(() -> {
+ // And stop the scan for routes
+ // MediaRouter should never be null, since all callbacks are be removed in destroy()
+ Objects.requireNonNull(getMediaRouter()).removeCallback(callback);
+ // Notify
+ if (onTimeout != null) {
+ onTimeout.run();
+ }
+ }, timeout);
+ }
+ });
+ }
+
+ /**
+ * Call to stop the active scan if any exist.
+ *
+ * @param callback the callback to stop and remove
+ * @param completionCallback called on completion
+ */
+ public void stopRouteScan(ScanCallback callback, Runnable completionCallback) {
+ if (callback == null || activity == null) {
+ if (completionCallback != null) {
+ completionCallback.run();
+ }
+ return;
+ }
+ activity.runOnUiThread(() -> {
+ callback.stop();
+ MediaRouter mediaRouter = getMediaRouter();
+ if (mediaRouter != null) {
+ mediaRouter.removeCallback(callback);
+ }
+ if (completionCallback != null) {
+ completionCallback.run();
+ }
+ });
+ }
+
+ /**
+ * Exits the current session.
+ *
+ * @param stopCasting should the receiver application be stopped as well?
+ * @param callback called with .success or .error depending on the initial result
+ */
+ void endSession(boolean stopCasting, JavascriptCallback callback) {
+ activity.runOnUiThread(new Runnable() {
+ public void run() {
+ getSessionManager().addSessionManagerListener(new SessionListener() {
+ @Override
+ public void onSessionEnded(@NonNull CastSession castSession, int error) {
+ getSessionManager().removeSessionManagerListener(this, CastSession.class);
+ media.setSession(null);
+ if (callback != null) {
+ callback.success();
+ }
+ listener.onSessionEnd(ChromecastUtilities.createSessionObject(castSession, stopCasting ? "stopped" : "disconnected"));
+ }
+ }, CastSession.class);
+
+ getSessionManager().endCurrentSession(stopCasting);
+ }
+ });
+ }
+
+ public void destroy() {
+ getSessionManager().removeSessionManagerListener(newConnectionListener, CastSession.class);
+ handler.removeCallbacksAndMessages(null);
+ activity = null;
+ }
+
+ /**
+ * Create this empty class so that we don't have to override every function
+ * each time we need a SessionManagerListener.
+ */
+ private static class SessionListener implements SessionManagerListener {
+ @Override
+ public void onSessionStarting(@NonNull CastSession castSession) {
+ }
+
+ @Override
+ public void onSessionStarted(@NonNull CastSession castSession, @NonNull String sessionId) {
+ }
+
+ @Override
+ public void onSessionStartFailed(@NonNull CastSession castSession, int error) {
+ }
+
+ @Override
+ public void onSessionEnding(@NonNull CastSession castSession) {
+ }
+
+ @Override
+ public void onSessionEnded(@NonNull CastSession castSession, int error) {
+ }
+
+ @Override
+ public void onSessionResuming(@NonNull CastSession castSession, @NonNull String sessionId) {
+ }
+
+ @Override
+ public void onSessionResumed(@NonNull CastSession castSession, boolean wasSuspended) {
+ }
+
+ @Override
+ public void onSessionResumeFailed(@NonNull CastSession castSession, int error) {
+ }
+
+ @Override
+ public void onSessionSuspended(@NonNull CastSession castSession, int reason) {
+ }
+ }
+
+ interface SelectRouteCallback {
+ void onJoin(JSONObject jsonSession);
+
+ void onError(JSONObject message);
+ }
+
+ abstract static class RequestSessionCallback implements ConnectionCallback {
+ abstract void onError(int errorCode);
+
+ abstract void onCancel();
+
+ @Override
+ public final boolean onSessionEndedBeforeStart(int errorCode) {
+ onSessionStartFailed(errorCode);
+ return true;
+ }
+
+ @Override
+ public final boolean onSessionStartFailed(int errorCode) {
+ onError(errorCode);
+ return true;
+ }
+ }
+
+ interface ConnectionCallback {
+ /**
+ * Successfully joined a session on a route.
+ *
+ * @param jsonSession the session we joined
+ */
+ void onJoin(JSONObject jsonSession);
+
+ /**
+ * Called if we received an error.
+ *
+ * @param errorCode You can find the error meaning here:
+ * https://developers.google.com/android/reference/com/google/android/gms/cast/CastStatusCodes
+ * @return true if we are done listening for join, false, if we to keep listening
+ */
+ boolean onSessionStartFailed(int errorCode);
+
+ /**
+ * Called when we detect a session ended event before session started.
+ * See issues:
+ * https://github.com/jellyfin/cordova-plugin-chromecast/issues/49
+ * https://github.com/jellyfin/cordova-plugin-chromecast/issues/48
+ *
+ * @param errorCode error to output
+ * @return true if we are done listening for join, false, if we to keep listening
+ */
+ boolean onSessionEndedBeforeStart(int errorCode);
+ }
+
+ public abstract static class ScanCallback extends MediaRouter.Callback {
+ /**
+ * Called whenever a route is updated.
+ *
+ * @param routes the currently available routes
+ */
+ abstract void onRouteUpdate(List routes);
+
+ /**
+ * records whether we have been stopped or not.
+ */
+ private boolean stopped = false;
+ /**
+ * Global mediaRouter object.
+ */
+ private MediaRouter mediaRouter;
+
+ /**
+ * Sets the mediaRouter object.
+ *
+ * @param router mediaRouter object
+ */
+ void setMediaRouter(MediaRouter router) {
+ this.mediaRouter = router;
+ }
+
+ /**
+ * Call this method when you wish to stop scanning.
+ * It is important that it is called, otherwise battery
+ * life will drain more quickly.
+ */
+ void stop() {
+ stopped = true;
+ }
+
+ private void onFilteredRouteUpdate() {
+ if (stopped || mediaRouter == null) {
+ return;
+ }
+ List outRoutes = new ArrayList<>();
+ // Filter the routes
+ for (RouteInfo route : mediaRouter.getRoutes()) {
+ // We don't want default routes, or duplicate active routes
+ // or multizone duplicates https://github.com/jellyfin/cordova-plugin-chromecast/issues/32
+ Bundle extras = route.getExtras();
+ if (extras != null) {
+ CastDevice.getFromBundle(extras);
+ if (extras.getString("com.google.android.gms.cast.EXTRA_SESSION_ID") != null) {
+ continue;
+ }
+ }
+ if (!route.isDefault() && !Objects.equals(route.getDescription(), "Google Cast Multizone Member") && route.getPlaybackType() == RouteInfo.PLAYBACK_TYPE_REMOTE) {
+ outRoutes.add(route);
+ }
+ }
+ onRouteUpdate(outRoutes);
+ }
+
+ @Override
+ public final void onRouteAdded(@NonNull MediaRouter router, @NonNull RouteInfo route) {
+ onFilteredRouteUpdate();
+ }
+
+ @Override
+ public final void onRouteChanged(@NonNull MediaRouter router, @NonNull RouteInfo route) {
+ onFilteredRouteUpdate();
+ }
+
+ @Override
+ public final void onRouteRemoved(@NonNull MediaRouter router, @NonNull RouteInfo route) {
+ onFilteredRouteUpdate();
+ }
+ }
+
+ abstract static class Listener implements CastStateListener, ChromecastSession.Listener {
+ abstract void onReceiverAvailableUpdate(boolean available);
+
+ abstract void onSessionRejoin(JSONObject jsonSession);
+
+ /**
+ * CastStateListener functions.
+ */
+ @Override
+ public void onCastStateChanged(int state) {
+ onReceiverAvailableUpdate(state != CastState.NO_DEVICES_AVAILABLE);
+ }
+ }
+}
diff --git a/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastSession.java b/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastSession.java
new file mode 100644
index 0000000..9b233ff
--- /dev/null
+++ b/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastSession.java
@@ -0,0 +1,757 @@
+package org.jellyfin.mobile.player.cast;
+
+import android.app.Activity;
+
+import androidx.annotation.NonNull;
+
+import com.google.android.gms.cast.ApplicationMetadata;
+import com.google.android.gms.cast.Cast;
+import com.google.android.gms.cast.MediaInfo;
+import com.google.android.gms.cast.MediaLoadRequestData;
+import com.google.android.gms.cast.MediaQueueItem;
+import com.google.android.gms.cast.MediaSeekOptions;
+import com.google.android.gms.cast.MediaStatus;
+import com.google.android.gms.cast.framework.CastSession;
+import com.google.android.gms.cast.framework.media.MediaQueue;
+import com.google.android.gms.cast.framework.media.RemoteMediaClient;
+import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult;
+import com.google.android.gms.common.api.ResultCallback;
+
+import org.jellyfin.mobile.bridge.JavascriptCallback;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/*
+ * All of the Chromecast session specific functions should start here.
+ */
+public class ChromecastSession {
+ /**
+ * The current context.
+ */
+ private Activity activity;
+ /**
+ * A registered callback that we will un-register and re-register each time the session changes.
+ */
+ private Listener clientListener;
+ /**
+ * The current session.
+ */
+ private CastSession session;
+ /**
+ * The current session's client for controlling playback.
+ */
+ private RemoteMediaClient client;
+ /**
+ * Indicates whether we are requesting media or not.
+ */
+ private boolean requestingMedia = false;
+ /**
+ * Handles and used to trigger queue updates.
+ */
+ private MediaQueueController mediaQueueCallback;
+ /**
+ * Stores a callback that should be called when the queue is loaded.
+ */
+ private Runnable queueReloadCallback;
+ /**
+ * Stores a callback that should be called when the queue status is updated.
+ */
+ private Runnable queueStatusUpdatedCallback;
+
+ /**
+ * ChromecastSession constructor.
+ *
+ * @param act the current activity
+ * @param listener callback that will notify of certain events
+ */
+ public ChromecastSession(Activity act, @NonNull Listener listener) {
+ this.activity = act;
+ this.clientListener = listener;
+ }
+
+ /**
+ * Sets the session object the will be used for other commands in this class.
+ *
+ * @param castSession the session to use
+ */
+ public void setSession(CastSession castSession) {
+ activity.runOnUiThread(() -> {
+ if (castSession == null) {
+ client = null;
+ return;
+ }
+ if (castSession.equals(session)) {
+ // Don't client and listeners if session did not change
+ return;
+ }
+ session = castSession;
+ client = session.getRemoteMediaClient();
+ if (client == null) {
+ return;
+ }
+ setupQueue();
+ client.registerCallback(new RemoteMediaClient.Callback() {
+ private Integer prevItemId;
+
+ @Override
+ public void onStatusUpdated() {
+ MediaStatus status = client.getMediaStatus();
+ if (requestingMedia
+ || queueStatusUpdatedCallback != null
+ || queueReloadCallback != null) {
+ return;
+ }
+
+ if (status != null) {
+ if (prevItemId == null) {
+ prevItemId = status.getCurrentItemId();
+ }
+ boolean shouldSkipUpdate = false;
+ if (status.getPlayerState() == MediaStatus.PLAYER_STATE_LOADING) {
+ // It appears the queue has advanced to the next item
+ // So send an update to indicate the previous has finished
+ clientListener.onMediaUpdate(createMediaObject(MediaStatus.IDLE_REASON_FINISHED));
+ shouldSkipUpdate = true;
+ }
+ if (prevItemId != null && prevItemId != status.getCurrentItemId() && mediaQueueCallback.getCurrentItemIndex() != -1) {
+ // The currentItem has changed, so update the current queue items
+ setQueueReloadCallback(() -> prevItemId = status.getCurrentItemId());
+ mediaQueueCallback.refreshQueueItems();
+ shouldSkipUpdate = true;
+ }
+ if (shouldSkipUpdate) {
+ return;
+ }
+ }
+ // Send update
+ clientListener.onMediaUpdate(createMediaObject());
+ }
+
+ @Override
+ public void onQueueStatusUpdated() {
+ if (queueStatusUpdatedCallback != null) {
+ queueStatusUpdatedCallback.run();
+ setQueueStatusUpdatedCallback(null);
+ }
+ }
+ });
+ session.addCastListener(new Cast.Listener() {
+ @Override
+ public void onApplicationStatusChanged() {
+ clientListener.onSessionUpdate(createSessionObject());
+ }
+
+ @Override
+ public void onApplicationMetadataChanged(ApplicationMetadata appMetadata) {
+ clientListener.onSessionUpdate(createSessionObject());
+ }
+
+ @Override
+ public void onApplicationDisconnected(int i) {
+ clientListener.onSessionEnd(
+ ChromecastUtilities.createSessionObject(session, "stopped"));
+ }
+
+ @Override
+ public void onActiveInputStateChanged(int i) {
+ clientListener.onSessionUpdate(createSessionObject());
+ }
+
+ @Override
+ public void onStandbyStateChanged(int i) {
+ clientListener.onSessionUpdate(createSessionObject());
+ }
+
+ @Override
+ public void onVolumeChanged() {
+ clientListener.onSessionUpdate(createSessionObject());
+ }
+ });
+ });
+ }
+
+ /**
+ * Adds a message listener if one does not already exist.
+ *
+ * @param namespace namespace
+ */
+ public void addMessageListener(String namespace) {
+ if (client == null || session == null) {
+ return;
+ }
+ activity.runOnUiThread(() -> {
+ try {
+ session.setMessageReceivedCallbacks(namespace, clientListener);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ /**
+ * Sends a message to a specified namespace.
+ *
+ * @param namespace namespace
+ * @param message the message to send
+ * @param callback called with success or error
+ */
+ public void sendMessage(String namespace, String message, JavascriptCallback callback) {
+ if (client == null || session == null) {
+ callback.error("session_error");
+ return;
+ }
+ activity.runOnUiThread(() -> session.sendMessage(namespace, message).setResultCallback(result -> {
+ if (result.isSuccess()) {
+ callback.success();
+ } else {
+ callback.error(result.toString());
+ }
+ }));
+ }
+
+ /* ------------------------------------ MEDIA FNs ------------------------------------------- */
+
+ /**
+ * Loads media over the media API.
+ *
+ * @param contentId - The URL of the content
+ * @param customData - CustomData
+ * @param contentType - The MIME type of the content
+ * @param duration - The length of the video (if known)
+ * @param streamType - The stream type
+ * @param autoPlay - Whether or not to start the video playing or not
+ * @param currentTime - Where in the video to begin playing from
+ * @param metadata - Metadata
+ * @param textTrackStyle - The text track style
+ * @param callback called with success or error
+ */
+ public void loadMedia(String contentId, JSONObject customData, String contentType, long duration, String streamType, boolean autoPlay, double currentTime, JSONObject metadata, JSONObject textTrackStyle, JavascriptCallback callback) {
+ if (client == null || session == null) {
+ callback.error("session_error");
+ return;
+ }
+ activity.runOnUiThread(() -> {
+ MediaInfo mediaInfo = ChromecastUtilities.createMediaInfo(contentId, customData, contentType, duration, streamType, metadata, textTrackStyle);
+ MediaLoadRequestData loadRequest = new MediaLoadRequestData.Builder()
+ .setMediaInfo(mediaInfo)
+ .setAutoplay(autoPlay)
+ .setCurrentTime((long) currentTime * 1000)
+ .build();
+
+ requestingMedia = true;
+ setQueueReloadCallback(() -> callback.success(createMediaObject()));
+ client.load(loadRequest).setResultCallback(result -> {
+ requestingMedia = false;
+ if (!result.getStatus().isSuccess()) {
+ callback.error("session_error");
+ setQueueReloadCallback(null);
+ }
+ });
+ });
+ }
+
+ /**
+ * Media API - Calls play on the current media.
+ *
+ * @param callback called with success or error
+ */
+ public void mediaPlay(JavascriptCallback callback) {
+ if (client == null || session == null) {
+ callback.error("session_error");
+ return;
+ }
+ activity.runOnUiThread(() -> client.play()
+ .setResultCallback(getResultCallback(callback, "Failed to play.")));
+ }
+
+ /**
+ * Media API - Calls pause on the current media.
+ *
+ * @param callback called with success or error
+ */
+ public void mediaPause(JavascriptCallback callback) {
+ if (client == null || session == null) {
+ callback.error("session_error");
+ return;
+ }
+ activity.runOnUiThread(() -> client.pause()
+ .setResultCallback(getResultCallback(callback, "Failed to pause.")));
+ }
+
+ /**
+ * Media API - Seeks the current playing media.
+ *
+ * @param seekPosition - Seconds to seek to
+ * @param resumeState - Resume state once seeking is complete: PLAYBACK_PAUSE or PLAYBACK_START
+ * @param callback called with success or error
+ */
+ public void mediaSeek(long seekPosition, String resumeState, JavascriptCallback callback) {
+ if (client == null || session == null) {
+ callback.error("session_error");
+ return;
+ }
+ activity.runOnUiThread(() -> {
+ int resState;
+ switch (resumeState) {
+ case "PLAYBACK_START":
+ resState = MediaSeekOptions.RESUME_STATE_PLAY;
+ break;
+ case "PLAYBACK_PAUSE":
+ resState = MediaSeekOptions.RESUME_STATE_PAUSE;
+ break;
+ default:
+ resState = MediaSeekOptions.RESUME_STATE_UNCHANGED;
+ }
+
+ client.seek(new MediaSeekOptions.Builder()
+ .setPosition(seekPosition)
+ .setResumeState(resState)
+ .build()
+ ).setResultCallback(getResultCallback(callback, "Failed to seek."));
+ });
+ }
+
+ /**
+ * Media API - Sets the volume on the current playing media object, NOT ON THE CHROMECAST DIRECTLY.
+ *
+ * @param level the level to set the volume to
+ * @param muted if true set the media to muted, else, unmute
+ * @param callback called with success or error
+ */
+ public void mediaSetVolume(Double level, Boolean muted, JavascriptCallback callback) {
+ if (client == null || session == null) {
+ callback.error("session_error");
+ return;
+ }
+ activity.runOnUiThread(() -> {
+ // Figure out the number of callbacks we expect to receive
+ int calls = 0;
+ if (level != null) {
+ calls++;
+ }
+ if (muted != null) {
+ calls++;
+ }
+ if (calls == 0) {
+ // No change
+ callback.success();
+ return;
+ }
+
+ // We need this callback so that we can wait for a variable number of calls to come back
+ final int expectedCalls = calls;
+ ResultCallback cb = new ResultCallback() {
+ private int callsCompleted = 0;
+ private String finalErr = null;
+
+ private void completionCall() {
+ callsCompleted++;
+ if (callsCompleted >= expectedCalls) {
+ // Both the setvolume an setMute have returned
+ if (finalErr != null) {
+ callback.error(finalErr);
+ } else {
+ callback.success();
+ }
+ }
+ }
+
+ @Override
+ public void onResult(@NonNull MediaChannelResult result) {
+ if (!result.getStatus().isSuccess()) {
+ if (finalErr == null) {
+ finalErr = "Failed to set media volume/mute state:\n";
+ }
+ JSONObject errorResult = result.getCustomData();
+ if (errorResult != null) {
+ finalErr += "\n" + errorResult;
+ }
+ }
+ completionCall();
+ }
+ };
+
+ if (level != null) {
+ client.setStreamVolume(level)
+ .setResultCallback(cb);
+ }
+ if (muted != null) {
+ client.setStreamMute(muted)
+ .setResultCallback(cb);
+ }
+ });
+ }
+
+ /**
+ * Media API - Stops and unloads the current playing media.
+ *
+ * @param callback called with success or error
+ */
+ public void mediaStop(JavascriptCallback callback) {
+ if (client == null || session == null) {
+ callback.error("session_error");
+ return;
+ }
+ activity.runOnUiThread(() -> client.stop()
+ .setResultCallback(getResultCallback(callback, "Failed to stop.")));
+ }
+
+ /**
+ * Handle track changed.
+ *
+ * @param activeTracksIds active track ids
+ * @param textTrackStyle track style
+ * @param callback called with success or error
+ */
+ public void mediaEditTracksInfo(long[] activeTracksIds, JSONObject textTrackStyle, JavascriptCallback callback) {
+ if (client == null || session == null) {
+ callback.error("session_error");
+ return;
+ }
+ activity.runOnUiThread(() -> {
+ client.setActiveMediaTracks(activeTracksIds)
+ .setResultCallback(getResultCallback(callback, "Failed to set active media tracks."));
+ client.setTextTrackStyle(ChromecastUtilities.parseTextTrackStyle(textTrackStyle))
+ .setResultCallback(getResultCallback(callback, "Failed to set text track style."));
+ });
+ }
+
+ /* ------------------------------------ QUEUE FNs ------------------------------------------- */
+
+ private void setQueueReloadCallback(Runnable callback) {
+ this.queueReloadCallback = callback;
+ }
+
+ private void setQueueStatusUpdatedCallback(Runnable callback) {
+ this.queueStatusUpdatedCallback = callback;
+ }
+
+ /**
+ * Sets up the objects and listeners required for queue functionality.
+ */
+ private void setupQueue() {
+ MediaQueue queue = client.getMediaQueue();
+ setQueueReloadCallback(null);
+ mediaQueueCallback = new MediaQueueController(queue);
+ queue.registerCallback(mediaQueueCallback);
+ }
+
+ private class MediaQueueController extends MediaQueue.Callback {
+ /**
+ * The MediaQueue object.
+ **/
+ private final MediaQueue queue;
+ /**
+ * Contains the item indexes that we need before sending out an update.
+ **/
+ private ArrayList lookingForIndexes = new ArrayList<>();
+ /**
+ * Keeps track of the queueItems.
+ **/
+ private JSONArray queueItems;
+
+ MediaQueueController(MediaQueue q) {
+ this.queue = q;
+ }
+
+ /**
+ * Given i == currentItemId, get items [i-1, i, i+1].
+ * Note: Exclude items out of range, eg. < 0 and > queue.length.
+ * Therefore, it is always 2-3 items (matches chrome desktop implementation).
+ */
+ void refreshQueueItems() {
+ int len = queue.getItemIds().length;
+ int index = getCurrentItemIndex();
+
+ // Reset lookingForIndexes
+ lookingForIndexes = new ArrayList<>();
+
+ // Only add indexes to look for it the currentItemIndex is valid
+ if (index != -1) {
+ // init i-1, i, i+1 (exclude items out of range), so always 2-3 items
+ for (int i = index - 1; i <= index + 1; i++) {
+ if (i >= 0 && i < len) {
+ lookingForIndexes.add(i);
+ }
+ }
+ }
+ checkLookingForIndexes();
+ }
+
+ private int getCurrentItemIndex() {
+ return queue.indexOfItemWithId(client.getMediaStatus().getCurrentItemId());
+ }
+
+ /**
+ * Works to get all items listed in lookingForIndexes.
+ * After all have been found, send out an update.
+ */
+ private void checkLookingForIndexes() {
+ // reset queueItems
+ queueItems = new JSONArray();
+
+ // Can we get all items in lookingForIndex?
+ MediaQueueItem item;
+ boolean foundAllIndexes = true;
+ for (int index : lookingForIndexes) {
+ item = queue.getItemAtIndex(index, true);
+ // If this returns null that means the item is not in the cache, which will
+ // trigger itemsUpdatedAtIndexes, which will trigger checkLookingForIndexes again
+ if (item != null) {
+ queueItems.put(ChromecastUtilities.createQueueItem(item, index));
+ } else {
+ foundAllIndexes = false;
+ }
+ }
+ if (foundAllIndexes) {
+ lookingForIndexes.clear();
+ updateFinished();
+ }
+ }
+
+ private void updateFinished() {
+ // Update the queueItems
+ ChromecastUtilities.setQueueItems(queueItems);
+ if (queueReloadCallback != null && queue.getItemCount() > 0) {
+ queueReloadCallback.run();
+ setQueueReloadCallback(null);
+ }
+ clientListener.onMediaUpdate(createMediaObject());
+ }
+
+ @Override
+ public void itemsReloaded() {
+ synchronized (queue) {
+ int itemCount = queue.getItemCount();
+ if (itemCount == 0) {
+ return;
+ }
+ if (queueReloadCallback == null) {
+ setQueueReloadCallback(() -> {
+ // This was externally loaded
+ clientListener.onMediaLoaded(createMediaObject());
+ });
+ }
+ refreshQueueItems();
+ }
+ }
+
+ @Override
+ public void itemsUpdatedAtIndexes(int[] ints) {
+ synchronized (queue) {
+ // Check if we were looking for all the ints
+ for (int i = 0; i < ints.length; i++) {
+ // If we weren't looking for an ints, that means it was changed
+ // (rather than just retrieved from the cache)
+ if (lookingForIndexes.indexOf(ints[i]) == -1) {
+ // So refresh the queue (the changed item might not be part
+ // of the items we want to output anyways, so let refresh
+ // handle it.
+ refreshQueueItems();
+ return;
+ }
+ }
+ // Else, we got new items from the cache
+ checkLookingForIndexes();
+ }
+ }
+
+ @Override
+ public void itemsInsertedInRange(int startIndex, int insertCount) {
+ synchronized (queue) {
+ refreshQueueItems();
+ }
+ }
+
+ @Override
+ public void itemsRemovedAtIndexes(int[] ints) {
+ synchronized (queue) {
+ refreshQueueItems();
+ }
+ }
+ }
+
+ /**
+ * Loads a queue of media to the Chromecast.
+ *
+ * @param queueLoadRequest chrome.cast.media.QueueLoadRequest
+ * @param callback called with success or error
+ */
+ public void queueLoad(JSONObject queueLoadRequest, JavascriptCallback callback) {
+ if (client == null || session == null) {
+ callback.error("session_error");
+ return;
+ }
+ activity.runOnUiThread(() -> {
+ try {
+ JSONArray qItems = queueLoadRequest.getJSONArray("items");
+ MediaQueueItem[] items = new MediaQueueItem[qItems.length()];
+ for (int i = 0; i < qItems.length(); i++) {
+ items[i] = ChromecastUtilities.createMediaQueueItem(qItems.getJSONObject(i));
+ }
+
+ int startIndex = queueLoadRequest.getInt("startIndex");
+ int repeatMode = ChromecastUtilities.getAndroidRepeatMode(queueLoadRequest.getString("repeatMode"));
+ long playPosition = Double.valueOf(items[startIndex].getStartTime() * 1000).longValue();
+ JSONObject customData = null;
+ try {
+ customData = queueLoadRequest.getJSONObject("customData");
+ } catch (JSONException ignored) {
+ }
+
+ setQueueReloadCallback(() -> callback.success(createMediaObject()));
+ client.queueLoad(items, startIndex, repeatMode, playPosition, customData).setResultCallback(result -> {
+ if (!result.getStatus().isSuccess()) {
+ callback.error("session_error");
+ setQueueReloadCallback(null);
+ }
+ });
+ } catch (JSONException e) {
+ callback.error(ChromecastUtilities.createError("invalid_parameter", e.getMessage()));
+ }
+ });
+ }
+
+ /**
+ * Plays the item with itemId in the queue.
+ *
+ * @param itemId The ID of the item to jump to.
+ * @param callback called with .success or .error depending on the result
+ */
+ public void queueJumpToItem(Integer itemId, JavascriptCallback callback) {
+ if (client == null || session == null) {
+ callback.error("session_error");
+ return;
+ }
+
+ activity.runOnUiThread(() -> {
+ setQueueStatusUpdatedCallback(() -> clientListener.onMediaUpdate(createMediaObject(MediaStatus.IDLE_REASON_INTERRUPTED)));
+ client.queueJumpToItem(itemId, null).setResultCallback(result -> {
+ if (result.getStatus().isSuccess()) {
+ callback.success();
+ } else {
+ setQueueStatusUpdatedCallback(null);
+ JSONObject errorResult = result.getCustomData();
+ String error = "Failed to jump to queue item with ID: " + itemId;
+ if (errorResult != null) {
+ error += "\nError details: " + errorResult;
+ }
+ callback.error(error);
+ }
+ });
+ });
+ }
+
+ /* ------------------------------------ SESSION FNs ------------------------------------------- */
+
+ /**
+ * Sets the receiver volume level.
+ *
+ * @param volume volume to set the receiver to
+ * @param callback called with success or error
+ */
+ public void setVolume(double volume, JavascriptCallback callback) {
+ if (client == null || session == null) {
+ callback.error("session_error");
+ return;
+ }
+ activity.runOnUiThread(() -> {
+ try {
+ session.setVolume(volume);
+ callback.success();
+ } catch (IOException e) {
+ callback.error("CHANNEL_ERROR");
+ }
+ });
+ }
+
+ /**
+ * Mutes the receiver.
+ *
+ * @param muted if true mute, else, unmute
+ * @param callback called with success or error
+ */
+ public void setMute(boolean muted, JavascriptCallback callback) {
+ if (client == null || session == null) {
+ callback.error("session_error");
+ return;
+ }
+ activity.runOnUiThread(() -> {
+ try {
+ session.setMute(muted);
+ callback.success();
+ } catch (IOException e) {
+ callback.error("CHANNEL_ERROR");
+ }
+ });
+ }
+
+ /* ------------------------------------ HELPERS ---------------------------------------------- */
+
+ /**
+ * Returns a resultCallback that wraps the callback and calls the onMediaUpdate listener.
+ *
+ * @param callback client callback
+ * @param errorMsg error message if failure
+ * @return a callback for use in PendingResult.setResultCallback()
+ */
+ private ResultCallback getResultCallback(JavascriptCallback callback, String errorMsg) {
+ return result -> {
+ if (result.getStatus().isSuccess()) {
+ callback.success();
+ } else {
+ JSONObject errorResult = result.getCustomData();
+ String error = errorMsg;
+ if (errorResult != null) {
+ error += "\nError details: " + errorMsg;
+ }
+ callback.error(error);
+ }
+ };
+ }
+
+ private JSONObject createSessionObject() {
+ return ChromecastUtilities.createSessionObject(session);
+ }
+
+ public void destroy() {
+ activity = null;
+ }
+
+ /**
+ * Last sent media object.
+ **/
+ private JSONObject lastMediaObject;
+
+ private JSONObject createMediaObject() {
+ return createMediaObject(null);
+ }
+
+ private JSONObject createMediaObject(Integer idleReason) {
+ if (idleReason != null && lastMediaObject != null) {
+ try {
+ lastMediaObject.put("playerState", ChromecastUtilities.getMediaPlayerState(MediaStatus.PLAYER_STATE_IDLE));
+ lastMediaObject.put("idleReason", ChromecastUtilities.getMediaIdleReason(idleReason));
+ return lastMediaObject;
+ } catch (JSONException ignored) {
+ }
+ }
+ JSONObject out = ChromecastUtilities.createMediaObject(session);
+ lastMediaObject = out;
+ return out;
+ }
+
+ interface Listener extends Cast.MessageReceivedCallback {
+ void onMediaLoaded(JSONObject jsonMedia);
+
+ void onMediaUpdate(JSONObject jsonMedia);
+
+ void onSessionUpdate(JSONObject jsonSession);
+
+ void onSessionEnd(JSONObject jsonSession);
+ }
+}
diff --git a/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastUtilities.java b/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastUtilities.java
new file mode 100644
index 0000000..bbdf8a2
--- /dev/null
+++ b/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastUtilities.java
@@ -0,0 +1,938 @@
+package org.jellyfin.mobile.player.cast;
+
+import android.annotation.SuppressLint;
+import android.graphics.Color;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.mediarouter.media.MediaRouter;
+
+import com.google.android.gms.cast.ApplicationMetadata;
+import com.google.android.gms.cast.CastDevice;
+import com.google.android.gms.cast.MediaInfo;
+import com.google.android.gms.cast.MediaMetadata;
+import com.google.android.gms.cast.MediaQueueData;
+import com.google.android.gms.cast.MediaQueueItem;
+import com.google.android.gms.cast.MediaStatus;
+import com.google.android.gms.cast.MediaTrack;
+import com.google.android.gms.cast.TextTrackStyle;
+import com.google.android.gms.cast.framework.CastSession;
+import com.google.android.gms.common.images.WebImage;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.GregorianCalendar;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+final class ChromecastUtilities {
+ /**
+ * Stores a cache of the queueItems for building Media Objects.
+ */
+ private static JSONArray queueItems = null;
+
+ private ChromecastUtilities() {
+ //not called
+ }
+
+ /**
+ * Sets the queueItems to be returned with the media object so they don't have to be calculated
+ * every time we need to send an update.
+ *
+ * @param items queueItems
+ */
+ static void setQueueItems(JSONArray items) {
+ queueItems = items;
+ }
+
+ static String getMediaIdleReason(int idleReason) {
+ switch (idleReason) {
+ case MediaStatus.IDLE_REASON_CANCELED:
+ return "CANCELLED";
+ case MediaStatus.IDLE_REASON_ERROR:
+ return "ERROR";
+ case MediaStatus.IDLE_REASON_FINISHED:
+ return "FINISHED";
+ case MediaStatus.IDLE_REASON_INTERRUPTED:
+ return "INTERRUPTED";
+ case MediaStatus.IDLE_REASON_NONE:
+ default:
+ return null;
+ }
+ }
+
+ static String getMediaPlayerState(int playerState) {
+ switch (playerState) {
+ case MediaStatus.PLAYER_STATE_LOADING:
+ case MediaStatus.PLAYER_STATE_BUFFERING:
+ return "BUFFERING";
+ case MediaStatus.PLAYER_STATE_IDLE:
+ return "IDLE";
+ case MediaStatus.PLAYER_STATE_PAUSED:
+ return "PAUSED";
+ case MediaStatus.PLAYER_STATE_PLAYING:
+ return "PLAYING";
+ case MediaStatus.PLAYER_STATE_UNKNOWN:
+ return "UNKNOWN";
+ default:
+ return null;
+ }
+ }
+
+ static String getMediaInfoStreamType(MediaInfo mediaInfo) {
+ switch (mediaInfo.getStreamType()) {
+ case MediaInfo.STREAM_TYPE_BUFFERED:
+ return "BUFFERED";
+ case MediaInfo.STREAM_TYPE_LIVE:
+ return "LIVE";
+ case MediaInfo.STREAM_TYPE_NONE:
+ return "OTHER";
+ default:
+ return null;
+ }
+ }
+
+ static String getTrackType(MediaTrack track) {
+ switch (track.getType()) {
+ case MediaTrack.TYPE_AUDIO:
+ return "AUDIO";
+ case MediaTrack.TYPE_TEXT:
+ return "TEXT";
+ case MediaTrack.TYPE_VIDEO:
+ return "VIDEO";
+ default:
+ return null;
+ }
+ }
+
+ static String getTrackSubtype(MediaTrack track) {
+ switch (track.getSubtype()) {
+ case MediaTrack.SUBTYPE_CAPTIONS:
+ return "CAPTIONS";
+ case MediaTrack.SUBTYPE_CHAPTERS:
+ return "CHAPTERS";
+ case MediaTrack.SUBTYPE_DESCRIPTIONS:
+ return "DESCRIPTIONS";
+ case MediaTrack.SUBTYPE_METADATA:
+ return "METADATA";
+ case MediaTrack.SUBTYPE_SUBTITLES:
+ return "SUBTITLES";
+ case MediaTrack.SUBTYPE_NONE:
+ default:
+ return null;
+ }
+ }
+
+ static String getEdgeType(TextTrackStyle textTrackStyle) {
+ switch (textTrackStyle.getEdgeType()) {
+ case TextTrackStyle.EDGE_TYPE_DEPRESSED:
+ return "DEPRESSED";
+ case TextTrackStyle.EDGE_TYPE_DROP_SHADOW:
+ return "DROP_SHADOW";
+ case TextTrackStyle.EDGE_TYPE_OUTLINE:
+ return "OUTLINE";
+ case TextTrackStyle.EDGE_TYPE_RAISED:
+ return "RAISED";
+ case TextTrackStyle.EDGE_TYPE_NONE:
+ default:
+ return "NONE";
+ }
+ }
+
+ static String getFontGenericFamily(TextTrackStyle textTrackStyle) {
+ switch (textTrackStyle.getFontGenericFamily()) {
+ case TextTrackStyle.FONT_FAMILY_CURSIVE:
+ return "CURSIVE";
+ case TextTrackStyle.FONT_FAMILY_MONOSPACED_SANS_SERIF:
+ return "MONOSPACED_SANS_SERIF";
+ case TextTrackStyle.FONT_FAMILY_MONOSPACED_SERIF:
+ return "MONOSPACED_SERIF";
+ case TextTrackStyle.FONT_FAMILY_SANS_SERIF:
+ return "SANS_SERIF";
+ case TextTrackStyle.FONT_FAMILY_SMALL_CAPITALS:
+ return "SMALL_CAPITALS";
+ case TextTrackStyle.FONT_FAMILY_SERIF:
+ default:
+ return "SERIF";
+ }
+ }
+
+ static String getFontStyle(TextTrackStyle textTrackStyle) {
+ switch (textTrackStyle.getFontStyle()) {
+ case TextTrackStyle.FONT_STYLE_BOLD:
+ return "BOLD";
+ case TextTrackStyle.FONT_STYLE_BOLD_ITALIC:
+ return "BOLD_ITALIC";
+ case TextTrackStyle.FONT_STYLE_ITALIC:
+ return "ITALIC";
+ case TextTrackStyle.FONT_STYLE_UNSPECIFIED:
+ case TextTrackStyle.FONT_STYLE_NORMAL:
+ default:
+ return "NORMAL";
+ }
+ }
+
+ static String getWindowType(TextTrackStyle textTrackStyle) {
+ switch (textTrackStyle.getWindowType()) {
+ case TextTrackStyle.WINDOW_TYPE_NORMAL:
+ return "NORMAL";
+ case TextTrackStyle.WINDOW_TYPE_ROUNDED:
+ return "ROUNDED_CORNERS";
+ case TextTrackStyle.WINDOW_TYPE_NONE:
+ default:
+ return "NONE";
+ }
+ }
+
+ static String getRepeatMode(int repeatMode) {
+ switch (repeatMode) {
+ case MediaStatus.REPEAT_MODE_REPEAT_OFF:
+ return "REPEAT_OFF";
+ case MediaStatus.REPEAT_MODE_REPEAT_ALL:
+ return "REPEAT_ALL";
+ case MediaStatus.REPEAT_MODE_REPEAT_SINGLE:
+ return "REPEAT_SINGLE";
+ case MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE:
+ return "REPEAT_ALL_AND_SHUFFLE";
+ default:
+ return null;
+ }
+ }
+
+ static int getAndroidRepeatMode(String clientRepeatMode) throws JSONException {
+ switch (clientRepeatMode) {
+ case "REPEAT_OFF":
+ return MediaStatus.REPEAT_MODE_REPEAT_OFF;
+ case "REPEAT_ALL":
+ return MediaStatus.REPEAT_MODE_REPEAT_ALL;
+ case "REPEAT_SINGLE":
+ return MediaStatus.REPEAT_MODE_REPEAT_SINGLE;
+ case "REPEAT_ALL_AND_SHUFFLE":
+ return MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE;
+ default:
+ throw new JSONException("Invalid repeat mode: " + clientRepeatMode);
+ }
+ }
+
+ static String getAndroidMetadataName(String clientName) {
+ switch (clientName) {
+ case "albumArtist":
+ return MediaMetadata.KEY_ALBUM_ARTIST;
+ case "albumName":
+ return MediaMetadata.KEY_ALBUM_TITLE;
+ case "artist":
+ return MediaMetadata.KEY_ARTIST;
+ case "bookTitle":
+ return MediaMetadata.KEY_BOOK_TITLE;
+ case "broadcastDate":
+ return MediaMetadata.KEY_BROADCAST_DATE;
+ case "chapterNumber":
+ return MediaMetadata.KEY_CHAPTER_NUMBER;
+ case "chapterTitle":
+ return MediaMetadata.KEY_CHAPTER_TITLE;
+ case "composer":
+ return MediaMetadata.KEY_COMPOSER;
+ case "creationDate":
+ case "creationDateTime":
+ return MediaMetadata.KEY_CREATION_DATE;
+ case "discNumber":
+ return MediaMetadata.KEY_DISC_NUMBER;
+ case "episode":
+ return MediaMetadata.KEY_EPISODE_NUMBER;
+ case "height":
+ return MediaMetadata.KEY_HEIGHT;
+ case "latitude":
+ return MediaMetadata.KEY_LOCATION_LATITUDE;
+ case "longitude":
+ return MediaMetadata.KEY_LOCATION_LONGITUDE;
+ case "locationName":
+ return MediaMetadata.KEY_LOCATION_NAME;
+ case "queueItemId":
+ return MediaMetadata.KEY_QUEUE_ITEM_ID;
+ case "releaseDate":
+ case "originalAirDate":
+ return MediaMetadata.KEY_RELEASE_DATE;
+ case "season":
+ return MediaMetadata.KEY_SEASON_NUMBER;
+ case "sectionDuration":
+ return MediaMetadata.KEY_SECTION_DURATION;
+ case "sectionStartAbsoluteTime":
+ return MediaMetadata.KEY_SECTION_START_ABSOLUTE_TIME;
+ case "sectionStartTimeInContainer":
+ return MediaMetadata.KEY_SECTION_START_TIME_IN_CONTAINER;
+ case "sectionStartTimeInMedia":
+ return MediaMetadata.KEY_SECTION_START_TIME_IN_MEDIA;
+ case "seriesTitle":
+ return MediaMetadata.KEY_SERIES_TITLE;
+ case "studio":
+ return MediaMetadata.KEY_STUDIO;
+ case "subtitle":
+ return MediaMetadata.KEY_SUBTITLE;
+ case "title":
+ return MediaMetadata.KEY_TITLE;
+ case "trackNumber":
+ return MediaMetadata.KEY_TRACK_NUMBER;
+ case "width":
+ return MediaMetadata.KEY_WIDTH;
+ default:
+ return clientName;
+ }
+ }
+
+ static String getClientMetadataName(String androidName) {
+ switch (androidName) {
+ case MediaMetadata.KEY_ALBUM_ARTIST:
+ return "albumArtist";
+ case MediaMetadata.KEY_ALBUM_TITLE:
+ return "albumName";
+ case MediaMetadata.KEY_ARTIST:
+ return "artist";
+ case MediaMetadata.KEY_BOOK_TITLE:
+ return "bookTitle";
+ case MediaMetadata.KEY_BROADCAST_DATE:
+ return "broadcastDate";
+ case MediaMetadata.KEY_CHAPTER_NUMBER:
+ return "chapterNumber";
+ case MediaMetadata.KEY_CHAPTER_TITLE:
+ return "chapterTitle";
+ case MediaMetadata.KEY_COMPOSER:
+ return "composer";
+ case MediaMetadata.KEY_CREATION_DATE:
+ return "creationDate";
+ case MediaMetadata.KEY_DISC_NUMBER:
+ return "discNumber";
+ case MediaMetadata.KEY_EPISODE_NUMBER:
+ return "episode";
+ case MediaMetadata.KEY_HEIGHT:
+ return "height";
+ case MediaMetadata.KEY_LOCATION_LATITUDE:
+ return "latitude";
+ case MediaMetadata.KEY_LOCATION_LONGITUDE:
+ return "longitude";
+ case MediaMetadata.KEY_LOCATION_NAME:
+ return "location";
+ case MediaMetadata.KEY_QUEUE_ITEM_ID:
+ return "queueItemId";
+ case MediaMetadata.KEY_RELEASE_DATE:
+ return "releaseDate";
+ case MediaMetadata.KEY_SEASON_NUMBER:
+ return "season";
+ case MediaMetadata.KEY_SECTION_DURATION:
+ return "sectionDuration";
+ case MediaMetadata.KEY_SECTION_START_ABSOLUTE_TIME:
+ return "sectionStartAbsoluteTime";
+ case MediaMetadata.KEY_SECTION_START_TIME_IN_CONTAINER:
+ return "sectionStartTimeInContainer";
+ case MediaMetadata.KEY_SECTION_START_TIME_IN_MEDIA:
+ return "sectionStartTimeInMedia";
+ case MediaMetadata.KEY_SERIES_TITLE:
+ return "seriesTitle";
+ case MediaMetadata.KEY_STUDIO:
+ return "studio";
+ case MediaMetadata.KEY_SUBTITLE:
+ return "subtitle";
+ case MediaMetadata.KEY_TITLE:
+ return "title";
+ case MediaMetadata.KEY_TRACK_NUMBER:
+ return "trackNumber";
+ case MediaMetadata.KEY_WIDTH:
+ return "width";
+ default:
+ return androidName;
+ }
+ }
+
+ static String getMetadataType(String androidName) {
+ switch (androidName) {
+ case MediaMetadata.KEY_ALBUM_ARTIST:
+ case MediaMetadata.KEY_ALBUM_TITLE:
+ case MediaMetadata.KEY_ARTIST:
+ case MediaMetadata.KEY_BOOK_TITLE:
+ case MediaMetadata.KEY_CHAPTER_NUMBER:
+ case MediaMetadata.KEY_CHAPTER_TITLE:
+ case MediaMetadata.KEY_COMPOSER:
+ case MediaMetadata.KEY_LOCATION_NAME:
+ case MediaMetadata.KEY_SERIES_TITLE:
+ case MediaMetadata.KEY_STUDIO:
+ case MediaMetadata.KEY_SUBTITLE:
+ case MediaMetadata.KEY_TITLE:
+ return "string"; // 1 in MediaMetadata
+ case MediaMetadata.KEY_DISC_NUMBER:
+ case MediaMetadata.KEY_EPISODE_NUMBER:
+ case MediaMetadata.KEY_HEIGHT:
+ case MediaMetadata.KEY_QUEUE_ITEM_ID:
+ case MediaMetadata.KEY_SEASON_NUMBER:
+ case MediaMetadata.KEY_TRACK_NUMBER:
+ case MediaMetadata.KEY_WIDTH:
+ return "int"; // 2 in MediaMetadata
+ case MediaMetadata.KEY_LOCATION_LATITUDE:
+ case MediaMetadata.KEY_LOCATION_LONGITUDE:
+ return "double"; // 3 in MediaMetadata
+ case MediaMetadata.KEY_BROADCAST_DATE:
+ case MediaMetadata.KEY_CREATION_DATE:
+ case MediaMetadata.KEY_RELEASE_DATE:
+ return "date"; // 4 in MediaMetadata
+ case MediaMetadata.KEY_SECTION_DURATION:
+ case MediaMetadata.KEY_SECTION_START_ABSOLUTE_TIME:
+ case MediaMetadata.KEY_SECTION_START_TIME_IN_CONTAINER:
+ case MediaMetadata.KEY_SECTION_START_TIME_IN_MEDIA:
+ return "ms"; // 5 in MediaMetadata
+ default:
+ return "custom";
+ }
+ }
+
+ static TextTrackStyle parseTextTrackStyle(JSONObject textTrackSytle) {
+ TextTrackStyle out = new TextTrackStyle();
+
+ if (textTrackSytle == null) {
+ return out;
+ }
+
+ try {
+ if (!textTrackSytle.isNull("backgroundColor")) {
+ out.setBackgroundColor(Color.parseColor(textTrackSytle.getString("backgroundColor")));
+ }
+
+ if (!textTrackSytle.isNull("edgeColor")) {
+ out.setEdgeColor(Color.parseColor(textTrackSytle.getString("edgeColor")));
+ }
+
+ if (!textTrackSytle.isNull("foregroundColor")) {
+ out.setForegroundColor(Color.parseColor(textTrackSytle.getString("foregroundColor")));
+ }
+ } catch (JSONException ignored) {
+ }
+ return out;
+ }
+
+ static String getHexColor(int color) {
+ return "#" + Integer.toHexString(color);
+ }
+
+ static JSONObject createSessionObject(CastSession session, String state) {
+ JSONObject s = createSessionObject(session);
+ if (state != null) {
+ try {
+ s.put("status", state);
+ } catch (JSONException ignored) {
+ }
+ }
+ return s;
+ }
+
+ static JSONObject createSessionObject(CastSession session) {
+ JSONObject out = new JSONObject();
+ try {
+ ApplicationMetadata metadata = session.getApplicationMetadata();
+ if (metadata != null) {
+ out.put("appId", metadata.getApplicationId());
+ out.put("appImages", createImagesArray(metadata.getImages()));
+ out.put("displayName", metadata.getName());
+ out.put("media", createMediaArray(session));
+ out.put("receiver", createReceiverObject(session));
+ out.put("sessionId", session.getSessionId());
+ }
+ } catch (JSONException | NullPointerException | IllegalStateException ignored) {
+ }
+ return out;
+ }
+
+ private static JSONArray createImagesArray(List images) throws JSONException {
+ JSONArray appImages = new JSONArray();
+ JSONObject img;
+ for (WebImage o : images) {
+ img = new JSONObject();
+ img.put("url", o.getUrl().toString());
+ appImages.put(img);
+ }
+ return appImages;
+ }
+
+ private static JSONObject createReceiverObject(CastSession session) {
+ JSONObject out = new JSONObject();
+ try {
+ out.put("friendlyName", session.getCastDevice().getFriendlyName());
+ out.put("label", session.getCastDevice().getDeviceId());
+
+ JSONObject volume = new JSONObject();
+ volume.put("level", session.getVolume());
+ volume.put("muted", session.isMute());
+ out.put("volume", volume);
+ } catch (JSONException | NullPointerException ignored) {
+ }
+ return out;
+ }
+
+ static JSONArray createMediaArray(CastSession session) {
+ JSONArray out = new JSONArray();
+ JSONObject mediaInfoObj = createMediaObject(session);
+ if (mediaInfoObj != null) {
+ out.put(mediaInfoObj);
+ }
+ return out;
+ }
+
+ static JSONObject createMediaObject(CastSession session) {
+ return createMediaObject(session, queueItems);
+ }
+
+ static JSONObject createMediaObject(CastSession session, JSONArray items) {
+ JSONObject out = new JSONObject();
+
+ try {
+ MediaStatus mediaStatus = session.getRemoteMediaClient().getMediaStatus();
+
+ // TODO: Missing attributes are commented out.
+ // These are returned by the chromecast desktop SDK, we should probbaly return them too
+ //out.put("breakStatus",);
+ out.put("currentItemId", mediaStatus.getCurrentItemId());
+ out.put("currentTime", mediaStatus.getStreamPosition() / 1000.0);
+ out.put("customData", mediaStatus.getCustomData());
+ //out.put("extendedStatus",);
+ String idleReason = ChromecastUtilities.getMediaIdleReason(mediaStatus.getIdleReason());
+ if (idleReason != null) {
+ out.put("idleReason", idleReason);
+ }
+ out.put("items", items);
+ out.put("isAlive", mediaStatus.getPlayerState() != MediaStatus.PLAYER_STATE_IDLE);
+ //out.put("liveSeekableRange",);
+ out.put("loadingItemId", mediaStatus.getLoadingItemId());
+ out.put("media", createMediaInfoObject(session.getRemoteMediaClient().getMediaInfo()));
+ out.put("mediaSessionId", 1);
+ out.put("playbackRate", mediaStatus.getPlaybackRate());
+ out.put("playerState", ChromecastUtilities.getMediaPlayerState(mediaStatus.getPlayerState()));
+ out.put("preloadedItemId", mediaStatus.getPreloadedItemId());
+ out.put("queueData", createQueueData(mediaStatus));
+ out.put("repeatMode", getRepeatMode(mediaStatus.getQueueRepeatMode()));
+ out.put("sessionId", session.getSessionId());
+ //out.put("supportedMediaCommands", );
+ //out.put("videoInfo", );
+
+ JSONObject volume = new JSONObject();
+ volume.put("level", mediaStatus.getStreamVolume());
+ volume.put("muted", mediaStatus.isMute());
+ out.put("volume", volume);
+ out.put("activeTrackIds", createActiveTrackIds(mediaStatus.getActiveTrackIds()));
+ } catch (JSONException ignored) {
+ } catch (NullPointerException e) {
+ return null;
+ }
+
+ return out;
+ }
+
+ private static JSONArray createActiveTrackIds(long[] activeTrackIds) {
+ JSONArray out = new JSONArray();
+ try {
+ if (activeTrackIds.length == 0) {
+ return null;
+ }
+ for (long id : activeTrackIds) {
+ out.put(id);
+ }
+ } catch (NullPointerException e) {
+ return null;
+ }
+ return out;
+ }
+
+ static JSONObject createQueueData(MediaStatus status) {
+ JSONObject out = new JSONObject();
+ try {
+ MediaQueueData data = status.getQueueData();
+ if (data == null) {
+ return null;
+ }
+ out.put("repeatMode", ChromecastUtilities.getRepeatMode(data.getRepeatMode()));
+ out.put("shuffle", data.getRepeatMode() == MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE);
+ out.put("startIndex", data.getStartIndex());
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new RuntimeException("See above stack trace for error: " + e.getMessage());
+ }
+ return out;
+ }
+
+ static JSONObject createQueueItem(@NonNull MediaQueueItem item, int orderId) {
+ JSONObject out = new JSONObject();
+ try {
+ out.put("activeTrackIds", createActiveTrackIds(item.getActiveTrackIds()));
+ out.put("autoplay", item.getAutoplay());
+ out.put("customData", item.getCustomData());
+ out.put("itemId", item.getItemId());
+ out.put("media", createMediaInfoObject(item.getMedia()));
+ out.put("orderId", orderId);
+ Double playbackDuration = item.getPlaybackDuration();
+ if (Double.isInfinite(playbackDuration)) {
+ playbackDuration = null;
+ }
+ out.put("playbackDuration", playbackDuration);
+ out.put("preloadTime", item.getPreloadTime());
+ Double startTime = item.getStartTime();
+ if (Double.isNaN(startTime)) {
+ startTime = null;
+ }
+ out.put("startTime", startTime);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ throw new RuntimeException("See above stack trace for error: " + e.getMessage());
+ }
+ return out;
+ }
+
+ private static JSONArray createMediaInfoTracks(MediaInfo mediaInfo) {
+ JSONArray out = new JSONArray();
+
+ try {
+ if (mediaInfo.getMediaTracks() == null) {
+ return out;
+ }
+
+ for (MediaTrack track : mediaInfo.getMediaTracks()) {
+ JSONObject jsonTrack = new JSONObject();
+
+
+ // TODO: Missing attributes are commented out.
+ // These are returned by the chromecast desktop SDK, we should probbaly return them too
+
+ jsonTrack.put("trackId", track.getId());
+ jsonTrack.put("customData", track.getCustomData());
+ jsonTrack.put("language", track.getLanguage());
+ jsonTrack.put("name", track.getName());
+ jsonTrack.put("subtype", ChromecastUtilities.getTrackSubtype(track));
+ jsonTrack.put("trackContentId", track.getContentId());
+ jsonTrack.put("trackContentType", track.getContentType());
+ jsonTrack.put("type", ChromecastUtilities.getTrackType(track));
+
+ out.put(jsonTrack);
+ }
+ } catch (JSONException | NullPointerException ignored) {
+ }
+
+ return out;
+ }
+
+ private static JSONObject createMediaInfoObject(MediaInfo mediaInfo) {
+ JSONObject out = new JSONObject();
+
+ try {
+ // TODO: Missing attributes are commented out.
+ // These are returned by the chromecast desktop SDK, we should probably return them too
+ //out.put("breakClips",);
+ //out.put("breaks",);
+ out.put("contentId", mediaInfo.getContentId());
+ out.put("contentType", mediaInfo.getContentType());
+ out.put("customData", mediaInfo.getCustomData());
+ out.put("duration", mediaInfo.getStreamDuration() / 1000.0);
+ //out.put("mediaCategory",);
+ out.put("metadata", createMetadataObject(mediaInfo.getMetadata()));
+ out.put("streamType", ChromecastUtilities.getMediaInfoStreamType(mediaInfo));
+ out.put("tracks", createMediaInfoTracks(mediaInfo));
+ out.put("textTrackStyle", ChromecastUtilities.createTextTrackObject(mediaInfo.getTextTrackStyle()));
+
+ } catch (JSONException | NullPointerException ignored) {
+ }
+
+ return out;
+ }
+
+ static JSONObject createMetadataObject(MediaMetadata metadata) {
+ JSONObject out = new JSONObject();
+ if (metadata == null) {
+ return out;
+ }
+ try {
+ try {
+ // Must be in own try catch
+ out.put("images", createImagesArray(metadata.getImages()));
+ } catch (Exception ignored) {
+ }
+ out.put("metadataType", metadata.getMediaType());
+ out.put("type", metadata.getMediaType());
+
+ Set keys = metadata.keySet();
+ String outKey;
+ // First translate and add the Android specific keys
+ for (String key : keys) {
+ outKey = ChromecastUtilities.getClientMetadataName(key);
+ if (outKey.equals(key) || outKey.equals("type")) {
+ continue;
+ }
+ switch (ChromecastUtilities.getMetadataType(key)) {
+ case "string":
+ out.put(outKey, metadata.getString(key));
+ break;
+ case "int":
+ out.put(outKey, metadata.getInt(key));
+ break;
+ case "double":
+ out.put(outKey, metadata.getDouble(key));
+ break;
+ case "date":
+ out.put(outKey, metadata.getDate(key).getTimeInMillis());
+ break;
+ case "ms":
+ out.put(outKey, metadata.getTimeMillis(key));
+ break;
+ default:
+ }
+ }
+ // Then add the non-Android specific keys ensuring we don't overwrite existing keys
+ for (String key : keys) {
+ outKey = ChromecastUtilities.getClientMetadataName(key);
+ if (!outKey.equals(key) || out.has(outKey) || outKey.equals("type")) {
+ continue;
+ }
+ if (outKey.startsWith("cordova-plugin-chromecast_metadata_key=")) {
+ outKey = outKey.substring("cordova-plugin-chromecast_metadata_key=".length());
+ }
+ out.put(outKey, metadata.getString(key));
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ return out;
+ }
+
+ private static JSONObject createTextTrackObject(TextTrackStyle textTrackStyle) {
+ if (textTrackStyle == null) {
+ return null;
+ }
+ JSONObject out = new JSONObject();
+ try {
+ out.put("backgroundColor", getHexColor(textTrackStyle.getBackgroundColor()));
+ out.put("customData", textTrackStyle.getCustomData());
+ out.put("edgeColor", getHexColor(textTrackStyle.getEdgeColor()));
+ out.put("edgeType", getEdgeType(textTrackStyle));
+ out.put("fontFamily", textTrackStyle.getFontFamily());
+ out.put("fontGenericFamily", getFontGenericFamily(textTrackStyle));
+ out.put("fontScale", textTrackStyle.getFontScale());
+ out.put("fontStyle", getFontStyle(textTrackStyle));
+ out.put("foregroundColor", getHexColor(textTrackStyle.getForegroundColor()));
+ out.put("windowColor", getHexColor(textTrackStyle.getWindowColor()));
+ out.put("windowRoundedCornerRadius", textTrackStyle.getWindowCornerRadius());
+ out.put("windowType", getWindowType(textTrackStyle));
+ } catch (JSONException ignored) {
+ }
+ return out;
+ }
+
+ /**
+ * Simple helper to convert a route to JSON for passing down to the javascript side.
+ *
+ * @param routes the routes to convert
+ * @return a JSON Array of JSON representations of the routes
+ */
+ @SuppressLint("RestrictedApi")
+ static JSONArray createRoutesArray(List routes) {
+ JSONArray routesArray = new JSONArray();
+ for (MediaRouter.RouteInfo route : routes) {
+ try {
+ JSONObject obj = new JSONObject();
+ obj.put("name", route.getName());
+ obj.put("id", route.getId());
+
+ CastDevice device = CastDevice.getFromBundle(route.getExtras());
+ if (device != null) {
+ obj.put("isNearbyDevice", !device.isOnLocalNetwork());
+ obj.put("isCastGroup", route.isGroup());
+ }
+
+ routesArray.put(obj);
+ } catch (JSONException ignored) {
+ }
+ }
+ return routesArray;
+ }
+
+ static JSONObject createError(String code, String message) {
+ JSONObject out = new JSONObject();
+ try {
+ out.put("code", code);
+ out.put("description", message);
+ } catch (JSONException ignored) {
+ }
+ return out;
+ }
+
+ /* ------------------- Create NON-JSON (non-output) Objects ---------------------------------- */
+
+ /**
+ * Creates a MediaQueueItem from a JSONObject representation of a MediaQueueItem.
+ *
+ * @param mediaQueueItem a JSONObject representation of a MediaQueueItem
+ * @return a MediaQueueItem
+ * @throws JSONException If the input mediaQueueItem is incorrect
+ */
+ static MediaQueueItem createMediaQueueItem(JSONObject mediaQueueItem) throws JSONException {
+ MediaInfo mediaInfo = createMediaInfo(mediaQueueItem.getJSONObject("media"));
+ MediaQueueItem.Builder builder = new MediaQueueItem.Builder(mediaInfo);
+
+ try {
+ long[] activeTrackIds;
+ JSONArray trackIds = mediaQueueItem.getJSONArray("activeTrackIds");
+ activeTrackIds = new long[trackIds.length()];
+ for (int i = 0; i < trackIds.length(); i++) {
+ activeTrackIds[i] = trackIds.getLong(i);
+ }
+ builder.setActiveTrackIds(activeTrackIds);
+ } catch (JSONException ignored) {
+ }
+ try {
+ builder.setAutoplay(mediaQueueItem.getBoolean("autoplay"));
+ } catch (JSONException ignored) {
+ }
+ try {
+ builder.setPlaybackDuration(mediaQueueItem.getDouble("playbackDuration"));
+ } catch (JSONException ignored) {
+ }
+ try {
+ builder.setPreloadTime(mediaQueueItem.getDouble("preloadTime"));
+ } catch (JSONException ignored) {
+ }
+ try {
+ builder.setStartTime(mediaQueueItem.getDouble("startTime"));
+ } catch (JSONException ignored) {
+ }
+ return builder.build();
+ }
+
+ static MediaInfo createMediaInfo(JSONObject mediaInfo) {
+ String contentId = mediaInfo.optString("contentId", "");
+ JSONObject customData = mediaInfo.optJSONObject("customData");
+ if (customData == null) customData = new JSONObject();
+ String contentType = mediaInfo.optString("contentType", "unknown");
+ long duration = mediaInfo.optLong("duration");
+ String streamType = mediaInfo.optString("streamType", "unknown");
+ JSONObject metadata = mediaInfo.optJSONObject("metadata");
+ if (metadata == null) metadata = new JSONObject();
+ JSONObject textTrackStyle = mediaInfo.optJSONObject("textTrackStyle");
+ if (textTrackStyle == null) textTrackStyle = new JSONObject();
+ return createMediaInfo(contentId, customData, contentType, duration, streamType, metadata, textTrackStyle);
+ }
+
+ static MediaInfo createMediaInfo(String contentId, JSONObject customData, String contentType, long duration, String streamType, JSONObject metadata, JSONObject textTrackStyle) {
+ MediaInfo.Builder mediaInfoBuilder = new MediaInfo.Builder(contentId);
+
+ mediaInfoBuilder.setMetadata(createMediaMetadata(metadata));
+
+ int intStreamType;
+ switch (streamType) {
+ case "buffered":
+ intStreamType = MediaInfo.STREAM_TYPE_BUFFERED;
+ break;
+ case "live":
+ intStreamType = MediaInfo.STREAM_TYPE_LIVE;
+ break;
+ default:
+ intStreamType = MediaInfo.STREAM_TYPE_NONE;
+ }
+
+ TextTrackStyle trackStyle = ChromecastUtilities.parseTextTrackStyle(textTrackStyle);
+
+ mediaInfoBuilder
+ .setContentType(contentType)
+ .setCustomData(customData)
+ .setStreamType(intStreamType)
+ .setStreamDuration(duration)
+ .setTextTrackStyle(trackStyle);
+
+ return mediaInfoBuilder.build();
+ }
+
+ private static MediaMetadata createMediaMetadata(JSONObject metadata) {
+
+ MediaMetadata mediaMetadata;
+ try {
+ mediaMetadata = new MediaMetadata(metadata.getInt("metadataType"));
+ } catch (JSONException e) {
+ mediaMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
+ }
+ // Add any images
+ try {
+ JSONArray images = metadata.getJSONArray("images");
+ for (int i = 0; i < images.length(); i++) {
+ JSONObject imageObj = images.getJSONObject(i);
+ try {
+ Uri imageURI = Uri.parse(imageObj.getString("url"));
+ mediaMetadata.addImage(new WebImage(imageURI));
+ } catch (Exception ignored) {
+ }
+ }
+ } catch (JSONException ignored) {
+ }
+
+ // Dynamically add other parameters
+ Iterator keys = metadata.keys();
+ String key;
+ String convertedKey;
+ Object value;
+ while (keys.hasNext()) {
+ key = keys.next();
+ if (key.equals("metadataType")
+ || key.equals("images")
+ || key.equals("type")) {
+ continue;
+ }
+ try {
+ value = metadata.get(key);
+ convertedKey = ChromecastUtilities.getAndroidMetadataName(key);
+ // Try to add the translated version of the key
+ switch (ChromecastUtilities.getMetadataType(convertedKey)) {
+ case "string":
+ mediaMetadata.putString(convertedKey, metadata.getString(key));
+ break;
+ case "int":
+ mediaMetadata.putInt(convertedKey, metadata.getInt(key));
+ break;
+ case "double":
+ mediaMetadata.putDouble(convertedKey, metadata.getDouble(key));
+ break;
+ case "date":
+ GregorianCalendar c = new GregorianCalendar();
+ if (value instanceof java.lang.Integer
+ || value instanceof java.lang.Long
+ || value instanceof java.lang.Float
+ || value instanceof java.lang.Double) {
+ c.setTimeInMillis(metadata.getLong(key));
+ mediaMetadata.putDate(convertedKey, c);
+ } else {
+ String stringValue;
+ try {
+ stringValue = " value: " + metadata.getString(key);
+ } catch (JSONException e) {
+ stringValue = "";
+ }
+ new Error("Cannot date from metadata key: " + key + stringValue
+ + "\n Dates must be in milliseconds from epoch UTC")
+ .printStackTrace();
+ }
+ break;
+ case "ms":
+ mediaMetadata.putTimeMillis(convertedKey, metadata.getLong(key));
+ break;
+ default:
+ }
+ // Also always add the client's version of the key because sometimes the
+ // MediaMetadata object removes some parameters.
+ // eg. If you pass metadataType == 2 == MEDIA_TYPE_TV_SHOW you will lose any
+ // subtitle added for "com.google.android.gms.cast.metadata.SUBTITLE", but this
+ // is not in-line with chrome desktop which preserves the value.
+ if (!key.equals(convertedKey)) {
+ // It is is really stubborn and if you try to add the key "subtitle" that is
+ // also stripped. (Hence the "cordova-plugin-chromecast_metadata_key=" prefix
+ convertedKey = "cordova-plugin-chromecast_metadata_key=" + key;
+ }
+ mediaMetadata.putString(convertedKey, metadata.getString(key));
+ } catch (JSONException | IllegalArgumentException e) {
+ e.printStackTrace();
+ }
+ }
+ return mediaMetadata;
+ }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..cb4787f
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,26 @@
+allprojects {
+ repositories {
+ mavenCentral()
+ google()
+ mavenLocal {
+ content {
+ includeVersionByRegex(JellyfinSdk.GROUP, ".*", JellyfinSdk.LOCAL)
+ }
+ }
+ maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") {
+ content {
+ includeVersionByRegex(JellyfinSdk.GROUP, ".*", JellyfinSdk.SNAPSHOT)
+ includeVersionByRegex(JellyfinSdk.GROUP, ".*", JellyfinSdk.SNAPSHOT_UNSTABLE)
+ includeVersionByRegex(JellyfinExoPlayer.GROUP, ".*", JellyfinExoPlayer.SNAPSHOT)
+ }
+ }
+ }
+}
+
+tasks.wrapper {
+ distributionType = Wrapper.DistributionType.ALL
+}
+
+tasks.create("clean") {
+ delete(rootProject.buildDir)
+}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 0000000..876c922
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,7 @@
+plugins {
+ `kotlin-dsl`
+}
+
+repositories {
+ mavenCentral()
+}
diff --git a/buildSrc/src/main/kotlin/Jellyfin.kt b/buildSrc/src/main/kotlin/Jellyfin.kt
new file mode 100644
index 0000000..2a7bf85
--- /dev/null
+++ b/buildSrc/src/main/kotlin/Jellyfin.kt
@@ -0,0 +1,12 @@
+object JellyfinSdk {
+ const val GROUP = "org.jellyfin.sdk"
+
+ const val LOCAL = "latest-SNAPSHOT"
+ const val SNAPSHOT = "master-SNAPSHOT"
+ const val SNAPSHOT_UNSTABLE = "openapi-unstable-SNAPSHOT"
+}
+
+object JellyfinExoPlayer {
+ const val GROUP = "org.jellyfin.exoplayer"
+ const val SNAPSHOT = "SNAPSHOT"
+}
diff --git a/buildSrc/src/main/kotlin/SigningHelper.kt b/buildSrc/src/main/kotlin/SigningHelper.kt
new file mode 100644
index 0000000..5881768
--- /dev/null
+++ b/buildSrc/src/main/kotlin/SigningHelper.kt
@@ -0,0 +1,49 @@
+import org.gradle.api.Project
+import java.io.File
+import java.util.*
+
+object SigningHelper {
+
+ fun loadSigningConfig(project: Project): Config? {
+ val serializedKeystore = System.getenv("KEYSTORE") ?: return null
+ val storeFile = try {
+ project.file("/tmp/keystore.jks").apply {
+ writeBytes(Base64.getDecoder().decode(serializedKeystore))
+ }
+ } catch (e: RuntimeException) {
+ return null
+ }
+ val storePassword = System.getenv("KEYSTORE_PASSWORD") ?: return null
+ val keyAlias = System.getenv("KEY_ALIAS") ?: return null
+ val keyPassword = System.getenv("KEY_PASSWORD") ?: return null
+
+ return Config(
+ storeFile,
+ storePassword,
+ keyAlias,
+ keyPassword
+ )
+ }
+
+ data class Config(
+ /**
+ * Store file used when signing.
+ */
+ val storeFile: File,
+
+ /**
+ * Store password used when signing.
+ */
+ val storePassword: String,
+
+ /**
+ * Key alias used when signing.
+ */
+ val keyAlias: String,
+
+ /**
+ * Key password used when signing.
+ */
+ val keyPassword: String
+ )
+}
diff --git a/buildSrc/src/main/kotlin/VersionUtils.kt b/buildSrc/src/main/kotlin/VersionUtils.kt
new file mode 100644
index 0000000..8c02a37
--- /dev/null
+++ b/buildSrc/src/main/kotlin/VersionUtils.kt
@@ -0,0 +1,69 @@
+import org.gradle.api.Project
+import java.util.*
+
+/**
+ * Get the version name from the current environment or use the fallback.
+ * It will look for a environment variable called JELLYFIN_VERSION first.
+ * Next it will look for a property called "jellyfin.version" and lastly it will use the fallback.
+ * If the version in the environment starts with a "v" prefix it will be removed.
+ *
+ * Sample output:
+ * v2.0.0 -> 2.0.0
+ * null -> 0.0.0-dev.1 (unless different fallback set)
+ */
+fun Project.getVersionName(fallback: String = "0.0.0-dev.1"): String {
+ val configuredVersion = System.getenv("JELLYFIN_VERSION")
+ ?: findProperty("jellyfin.version")?.toString()
+
+ return configuredVersion?.removePrefix("v") ?: fallback
+}
+
+/**
+ * Get the version code for a given semantic version.
+ * Does not validate the input and thus will throw an exception when parts are missing.
+ *
+ * The pre-release part ("-rc.1", "-beta.1" etc.) defaults to 99 when not specified.
+ *
+ * Sample output:
+ * MA.MI.PA-PR > MA MI PA PR
+ * 0.0.0-dev.1 > 1
+ * 0.0.0 > 99
+ * 1.1.1 > 1 01 01 99
+ * 0.7.0 > 7 00 99
+ * 99.99.99 > 99 99 99 99
+ * 2.0.0-rc.3 > 2 00 00 03
+ * 2.0.0 > 2 00 00 99
+ * 99.99.99-rc.1 > 99 99 99 01
+ */
+fun getVersionCode(versionName: String): Int {
+ // Split to core and pre release parts with a default for pre release (null)
+ val (versionCore, versionPreRelease) =
+ when (val index = versionName.indexOf('-')) {
+ // No pre-release part included
+ -1 -> versionName to null
+ // Pre-release part included
+ else -> versionName.substring(0, index) to
+ versionName.substring(index + 1, versionName.length)
+ }
+
+ // Parse core part
+ val (major, minor, patch) = versionCore
+ .splitToSequence('.')
+ .mapNotNull(String::toIntOrNull)
+ .take(3)
+ .toList()
+
+ // Parse pre release part (ignore type, only get the number)
+ val buildVersion = versionPreRelease
+ ?.substringAfter('.')
+ ?.let(String::toIntOrNull)
+
+ // Build code
+ var code = 0
+ code += major * 1000000 // Major (0-99)
+ code += minor * 10000 // Minor (0-99)
+ code += patch * 100 // Patch (0-99)
+ code += buildVersion ?: 99 // Pre release (0-99)
+
+ return code
+}
diff --git a/detekt.yml b/detekt.yml
new file mode 100644
index 0000000..11619b8
--- /dev/null
+++ b/detekt.yml
@@ -0,0 +1,62 @@
+build:
+ maxIssues: 0
+
+complexity:
+ CyclomaticComplexMethod:
+ ignoreSimpleWhenEntries: true
+ LongParameterList:
+ constructorThreshold: 10
+ functionThreshold: 10
+ TooManyFunctions:
+ thresholdInFiles: 16
+ thresholdInClasses: 20
+ thresholdInInterfaces: 8
+ thresholdInObjects: 16
+ thresholdInEnums: 8
+ ignoreOverridden: true
+ ignoreDeprecated: true
+
+exceptions:
+ SwallowedException:
+ active: false
+
+formatting:
+ MaximumLineLength:
+ # Already handled by detekt itself
+ active: false
+ NoWildcardImports:
+ # Already handled by detekt itself
+ active: false
+ PackageName:
+ # Already handled by detekt itself
+ active: false
+ TrailingCommaOnCallSite:
+ active: true
+ useTrailingCommaOnCallSite: true
+ TrailingCommaOnDeclarationSite:
+ active: true
+ useTrailingCommaOnDeclarationSite: true
+
+naming:
+ FunctionNaming:
+ ignoreAnnotated:
+ - Composable
+ PackageNaming:
+ # Package names must be lowercase letters
+ packagePattern: '[a-z]+(\.[a-z]+)*'
+
+performance:
+ SpreadOperator:
+ active: false
+
+style:
+ ForbiddenComment:
+ # Allow TODOs
+ values: [ 'FIXME:', 'STOPSHIP:' ]
+ LoopWithTooManyJumpStatements:
+ maxJumpCount: 2
+ MaxLineLength:
+ maxLineLength: 200
+ ReturnCount:
+ excludeGuardClauses: true
+ max: 3
diff --git a/fastlane/metadata/android/af/changelogs/default.txt b/fastlane/metadata/android/af/changelogs/default.txt
new file mode 100644
index 0000000..b13b94e
--- /dev/null
+++ b/fastlane/metadata/android/af/changelogs/default.txt
@@ -0,0 +1,3 @@
+Dankie dat jy Jellyfin vir Android gebruik! Dit word altyd aanbeveel om die nuutste vrygestelde Jellyfin Server te gebruik.
+
+Vir meer inligting, sien asseblief ons blog by jellyfin.org, of lees die volledige changelog op GitHub.
diff --git a/fastlane/metadata/android/af/full_description.txt b/fastlane/metadata/android/af/full_description.txt
new file mode 100644
index 0000000..92bc6ad
--- /dev/null
+++ b/fastlane/metadata/android/af/full_description.txt
@@ -0,0 +1,14 @@
+U media, op u voorwaardes.
+
+Die Jellyfin-projek is 'n oopbron, gratis sagtewaremediaserver. Geen fooie, geen dop, geen verborge agenda. Kry ons gratis bediener om al u klank, video, foto's en meer op een plek te versamel.
+
+Om die app te kan gebruik, moet u 'n Jellyfin-bediener laat instel en laat loop. Kom meer te wete op jellyfin.org .
+
+Met 'n Jellyfin-bediener kan u:
+
+* Kyk regstreekse TV en opnames van u Jellyfin-bediener (ekstra hardeware / dienste benodig)
+* Stroom na 'n Chromecast-toestel in u netwerk
+* Stroom u media na u Android-toestel
+* Kyk na u versameling in 'n gebruiksvriendelike koppelvlak
+
+Dit is die amptelike Jellyfin-metgesel-app vir Android. Dankie dat u Jellyfin gebruik!
diff --git a/fastlane/metadata/android/af/short_description.txt b/fastlane/metadata/android/af/short_description.txt
new file mode 100644
index 0000000..adc333b
--- /dev/null
+++ b/fastlane/metadata/android/af/short_description.txt
@@ -0,0 +1 @@
+Mobiele kliënt vir Jellyfin, die gratis sagteware media systeem
diff --git a/fastlane/metadata/android/af/title.txt b/fastlane/metadata/android/af/title.txt
new file mode 100644
index 0000000..8711c1e
--- /dev/null
+++ b/fastlane/metadata/android/af/title.txt
@@ -0,0 +1 @@
+Jellyfin - jou media in jou hande!
diff --git a/fastlane/metadata/android/ar/changelogs/default.txt b/fastlane/metadata/android/ar/changelogs/default.txt
new file mode 100644
index 0000000..20712a0
--- /dev/null
+++ b/fastlane/metadata/android/ar/changelogs/default.txt
@@ -0,0 +1,3 @@
+جزيل الشكر على استخدامك لتطبيق Jellyfin الخاص بالأندرويد! ننصح دائما باستخدام آخر تحديث.
+
+زر مدونتنا على jellyfin.org للمزيد من المعلومات، أو اقرأ جميع تغيرات الإصدار على صفحت التغيرات في موقع قيت-هب.
diff --git a/fastlane/metadata/android/ar/full_description.txt b/fastlane/metadata/android/ar/full_description.txt
new file mode 100644
index 0000000..78c64d8
--- /dev/null
+++ b/fastlane/metadata/android/ar/full_description.txt
@@ -0,0 +1,14 @@
+وسائطك بشروطك.
+
+مشروع Jellyfin هو مشروع مجاني و مفتوح المصدر لبناء خادم لعرض الوسائط. بلا رسوم، ولا تعقب، ولا أي أجندة خفية. ثبت تطبيقنا المجاني لتجمع جميع المقاطع الصوتية، والبصرية، والصور الخاصة بك في مكان واحد.
+
+لاستخدام تطبيق Jellyfin، لابد أن يكون لديك خادم Jellyfin يعمل من قبل ، للمزيد من المعلومات:jellyfin.org.
+
+يتيح لك خادم Jellyfin المزايا التالية:
+
+* يتيح مشاهدة البث التلفزيوني المباشر والمسلسلات وما شابهها المسجلة في خادم Jellyfin الخاص بك(تتطلب أجهزة/خدمات إضافية)
+* يتيح لك مشاهدة وسائطك من جهاز الـ Chromecast في منزلك
+* يتيح لك مشاهدة وسائطك من جهازك الأندرويد الخاص بك
+* عرض وسائطك في واجهة سهلة الاستخدام
+
+هذا هو تطبيق Jellyfin المرافق والرسمي لنظام الأندرويد. شكرا على استخدامكم لـ Jellyfin!
diff --git a/fastlane/metadata/android/ar/short_description.txt b/fastlane/metadata/android/ar/short_description.txt
new file mode 100644
index 0000000..f8c2846
--- /dev/null
+++ b/fastlane/metadata/android/ar/short_description.txt
@@ -0,0 +1 @@
+تطبيق Jellyfin للجوال، وهو التطبيق المجاني لعرض الوسائط
diff --git a/fastlane/metadata/android/ar/title.txt b/fastlane/metadata/android/ar/title.txt
new file mode 100644
index 0000000..ed52056
--- /dev/null
+++ b/fastlane/metadata/android/ar/title.txt
@@ -0,0 +1 @@
+Jellyfin - وسائطك بين يديك!
diff --git a/fastlane/metadata/android/be/short_description.txt b/fastlane/metadata/android/be/short_description.txt
new file mode 100644
index 0000000..4bbadb6
--- /dev/null
+++ b/fastlane/metadata/android/be/short_description.txt
@@ -0,0 +1 @@
+Мабільны кліент для Jellyfin, медыясістэма са свабодным праграмным забеспячэннем
diff --git a/fastlane/metadata/android/be/title.txt b/fastlane/metadata/android/be/title.txt
new file mode 100644
index 0000000..0a04498
--- /dev/null
+++ b/fastlane/metadata/android/be/title.txt
@@ -0,0 +1 @@
+Jellyfin - ваша медыя ў вашых руках!
diff --git a/fastlane/metadata/android/bg/title.txt b/fastlane/metadata/android/bg/title.txt
new file mode 100644
index 0000000..babf445
--- /dev/null
+++ b/fastlane/metadata/android/bg/title.txt
@@ -0,0 +1 @@
+Jellyfin - твоята медия в твоите ръце!
diff --git a/fastlane/metadata/android/ca/changelogs/default.txt b/fastlane/metadata/android/ca/changelogs/default.txt
new file mode 100644
index 0000000..d0a6532
--- /dev/null
+++ b/fastlane/metadata/android/ca/changelogs/default.txt
@@ -0,0 +1,3 @@
+Gràcies per utilitzar Jellyfin per a Android! Es recomana sempre utilitzar el servidor Jellyfin més recent.
+
+Per a obtenir més informació, si us plau, consulti el nostre blog a jellyfin.org, o llegeix el registre de canvis complet a GitHub.
diff --git a/fastlane/metadata/android/ca/full_description.txt b/fastlane/metadata/android/ca/full_description.txt
new file mode 100644
index 0000000..1b6f16d
--- /dev/null
+++ b/fastlane/metadata/android/ca/full_description.txt
@@ -0,0 +1,14 @@
+Els teus mitjans, els teus termes.
+
+El projecte de Jellyfin és un servidor de mitjans, de programari lliure i codi obert. Sense pagaments, seguiments o motius ocults. Obtingui el nostre servidor gratuït per a recopilar tots els teus àudios, vídeos, fotografies i més en un sol lloc.
+
+Per a utilitzar l'aplicació, has de tenir un servidor Jellyfin configurat i en funcionament. Més informació a jellyfin.org.
+
+Amb un servidor Jellyfin, pots:
+
+*Veure TV en directe i gravar sèries des del teu servidor Jellyfin (Es requereix equip/serveis addicionals)
+*Transmetre a un dispositiu Chromecast a la teva xarxa
+*Transmetre la seva multimèdia al seu dispositiu Android
+*Veure la teva col·lecció de mitjans en una interfície fàcil d'utilitzar
+
+Aquesta és l'aplicació oficial de Jellyfin per a Android. Gràcies per utilitzar Jellyfin!
diff --git a/fastlane/metadata/android/ca/short_description.txt b/fastlane/metadata/android/ca/short_description.txt
new file mode 100644
index 0000000..906672f
--- /dev/null
+++ b/fastlane/metadata/android/ca/short_description.txt
@@ -0,0 +1 @@
+Client mòbil per Jellyfin, el sistema multimèdia de programari lliure
diff --git a/fastlane/metadata/android/ca/title.txt b/fastlane/metadata/android/ca/title.txt
new file mode 100644
index 0000000..e29573c
--- /dev/null
+++ b/fastlane/metadata/android/ca/title.txt
@@ -0,0 +1 @@
+Jellyfin - la vostra multimèdia a les teves mans!
diff --git a/fastlane/metadata/android/cs/changelogs/default.txt b/fastlane/metadata/android/cs/changelogs/default.txt
new file mode 100644
index 0000000..024ef47
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/default.txt
@@ -0,0 +1,3 @@
+Děkujeme, že používáte Jellyfin pro Android! Vždy doporučujeme používat nejnovější verzi serveru Jellyfin.
+
+Další informace naleznete na našem blogu na adrese jellyfin.org nebo si přečtěte kompletní seznam změn na GitHubu.
diff --git a/fastlane/metadata/android/cs/full_description.txt b/fastlane/metadata/android/cs/full_description.txt
new file mode 100644
index 0000000..215736f
--- /dev/null
+++ b/fastlane/metadata/android/cs/full_description.txt
@@ -0,0 +1,14 @@
+Vaše média, za vašich podmínek.
+
+Projekt Jellyfin je bezplatný mediální server s otevřeným zdrojovým kódem. Bez poplatků, sledování, či nekalých zájmů. Náš bezplatný server umožňuje spravovat zvuk, video, fotky, a další média na jednom místě.
+
+Abyste mohli aplikaci používat, musíte mít k dispozici server Jellyfin. Další informace naleznete na webu jellyfin.org.
+
+Se serverem Jellyfin můžete:
+
+* Sledovat živé i nahrané televizní vysílání (vyžaduje dodatečný hardware/služby)
+* Streamovat do zařízení Chromecast na vaší síti
+* Streamovat vaše vlastní média do vašeho zařízení s Androidem
+* Zobrazit svou sbírku médií pomocí snadno použitelného rozhraní
+
+Toto je oficiální aplikace Jellyfin pro systém Android. Děkujeme, že používáte Jellyfin!
diff --git a/fastlane/metadata/android/cs/short_description.txt b/fastlane/metadata/android/cs/short_description.txt
new file mode 100644
index 0000000..1dc9d25
--- /dev/null
+++ b/fastlane/metadata/android/cs/short_description.txt
@@ -0,0 +1 @@
+Mobilní klient pro Jellyfin, bezplatný správce médií
diff --git a/fastlane/metadata/android/cs/title.txt b/fastlane/metadata/android/cs/title.txt
new file mode 100644
index 0000000..44dda8c
--- /dev/null
+++ b/fastlane/metadata/android/cs/title.txt
@@ -0,0 +1 @@
+Jellyfin - vaše média ve vašich rukou!
diff --git a/fastlane/metadata/android/cy/full_description.txt b/fastlane/metadata/android/cy/full_description.txt
new file mode 100644
index 0000000..2c35e7a
--- /dev/null
+++ b/fastlane/metadata/android/cy/full_description.txt
@@ -0,0 +1,14 @@
+Eich cyfryngau, ar eich telerau.
+
+Mae'r prosiect Jellyfin yn weinydd cyfryngau meddalwedd agored, rhad ac am ddim. Dim ffioedd, dim olrhain data, dim agenda gudd. Mynnwch ein gweinydd rhad ac am ddim i gasglu'ch holl sain, fideo, lluniau a mwy mewn un lle.
+
+I ddefnyddio'r app, rhaid bod gennych weinydd Jellyfin wedi'i sefydlu a'i redeg. Darganfyddwch fwy yn jellyfin.org.
+
+Gyda gweinydd Jellyfin, gallwch chi:
+
+* Gwylio Teledu Byw a sioeau wedi'u recordio gan eich gweinydd Jellyfin (angen caledwedd/gwasanaethau ychwanegol)
+* Ffrydiwch i ddyfais Chromecast ar eich rhwydwaith
+* Ffrydiwch eich cyfryngau i'ch dyfais Android
+* Gweld eich casgliad mewn rhyngwyneb hawdd i'w ddefnyddio
+
+Dyma ap swyddogol Jellyfin ar gyfer Android. Diolch am ddefnyddio Jellyfin!
diff --git a/fastlane/metadata/android/cy/short_description.txt b/fastlane/metadata/android/cy/short_description.txt
new file mode 100644
index 0000000..789db1d
--- /dev/null
+++ b/fastlane/metadata/android/cy/short_description.txt
@@ -0,0 +1 @@
+Gweinydd symudol ar gyfer Jellyfin, y system cyfryngau di-dâl
diff --git a/fastlane/metadata/android/cy/title.txt b/fastlane/metadata/android/cy/title.txt
new file mode 100644
index 0000000..21bb2da
--- /dev/null
+++ b/fastlane/metadata/android/cy/title.txt
@@ -0,0 +1 @@
+Jellyfin - eich cyfryngau chi!
diff --git a/fastlane/metadata/android/da/changelogs/default.txt b/fastlane/metadata/android/da/changelogs/default.txt
new file mode 100644
index 0000000..91c783b
--- /dev/null
+++ b/fastlane/metadata/android/da/changelogs/default.txt
@@ -0,0 +1,3 @@
+Tak for at du bruger Jellyfin til Android! Det anbefales stærkt at bruge den nyeste version af Jellyfin Server.
+
+For mere information, se venligst vores blog på jellyfin.org, eller læs den fulde changelog på GitHub.
diff --git a/fastlane/metadata/android/da/full_description.txt b/fastlane/metadata/android/da/full_description.txt
new file mode 100644
index 0000000..23da400
--- /dev/null
+++ b/fastlane/metadata/android/da/full_description.txt
@@ -0,0 +1,14 @@
+Dine medier, på dine vilkår.
+
+Jellyfin projektet er et open source, fri software medieserver. Ingen gebyrer, ingen sporing, ingen skjult agenda. Hent vores gratis server for at samle alt dit musik, film, billeder, og mere på et sted.
+
+For at bruge appen, skal du have adgang til en Jellyfin server. Læs mere på jellyfin.org.
+
+Med en Jellyfin server, kan du:
+
+* Se live TV og optagelser fra din Jellyfin server (yderligere hardware/tjenester er nødvendige)
+* Afspil til en Chromecast enhed på dit netværk
+* Afspil dine medier på din Android enhed
+* Se din samling i et nemt at bruge grænseflade
+
+Dette er den officielle Jellyfin app til Android. Tak fordi du bruger Jellyfin!
diff --git a/fastlane/metadata/android/da/short_description.txt b/fastlane/metadata/android/da/short_description.txt
new file mode 100644
index 0000000..cdecf49
--- /dev/null
+++ b/fastlane/metadata/android/da/short_description.txt
@@ -0,0 +1 @@
+Mobil klient til Jellyfin, det gratis medie system
diff --git a/fastlane/metadata/android/da/title.txt b/fastlane/metadata/android/da/title.txt
new file mode 100644
index 0000000..7642665
--- /dev/null
+++ b/fastlane/metadata/android/da/title.txt
@@ -0,0 +1 @@
+Jellyfin - Dine medier i dine hænder!
diff --git a/fastlane/metadata/android/de/changelogs/default.txt b/fastlane/metadata/android/de/changelogs/default.txt
new file mode 100644
index 0000000..bb80525
--- /dev/null
+++ b/fastlane/metadata/android/de/changelogs/default.txt
@@ -0,0 +1,3 @@
+Danke für das Benutzen von Jellyfin für Android! Es wird empfohlen den aktuellsten Jellyfin Server zu verwenden.
+
+Für zusätzliche Information besuchen Sie unseren Blog oder lesen das Changelog auf Github.
diff --git a/fastlane/metadata/android/de/full_description.txt b/fastlane/metadata/android/de/full_description.txt
new file mode 100644
index 0000000..f5aab9d
--- /dev/null
+++ b/fastlane/metadata/android/de/full_description.txt
@@ -0,0 +1,14 @@
+Deine Medien, zu Deinen Bedingungen.
+
+Das Jellyfin-Projekt ist ein Open-Source-Medienserver mit freier Software. Keine Gebühren, kein Tracking, keine versteckte Agenda. Hol Dir unseren kostenlosen Server, um all Deine Audio- und Videodaten, Fotos und mehr an einem Ort zu sammeln.
+
+Um die App zu verwenden, benötigst Du einen laufenden Jellyfin Server. Mehr dazu unter jellyfin.org.
+
+Mit dem Jellyfin Server kannst Du:
+
+* Live TV und aufgenommene Shows von Deinem Jellyfin-Server ansehen (benötigt zusätzliche Hard-/Software)
+* Zu einem Chromecast-Empfänger in Deinem Netzwerk streamen
+* Deine Medien zu Deinem Android-Gerät streamen
+* Deine Kollektion mit einer einfach zu benutzenden Oberfläche ansehen.
+
+Dies ist die offizielle Jellyfin Kompanien-App für Android. Danke, dass Du Jellyfin verwendest!
diff --git a/fastlane/metadata/android/de/short_description.txt b/fastlane/metadata/android/de/short_description.txt
new file mode 100644
index 0000000..cd080ac
--- /dev/null
+++ b/fastlane/metadata/android/de/short_description.txt
@@ -0,0 +1 @@
+Mobiler Client für Jellyfin, das freie Mediensystem
diff --git a/fastlane/metadata/android/de/title.txt b/fastlane/metadata/android/de/title.txt
new file mode 100644
index 0000000..4e765fe
--- /dev/null
+++ b/fastlane/metadata/android/de/title.txt
@@ -0,0 +1 @@
+Jellyfin – Deine Medien in Deiner Hand!
diff --git a/fastlane/metadata/android/el/changelogs/default.txt b/fastlane/metadata/android/el/changelogs/default.txt
new file mode 100644
index 0000000..41855c8
--- /dev/null
+++ b/fastlane/metadata/android/el/changelogs/default.txt
@@ -0,0 +1,3 @@
+Σας ευχαριστούμε που χρησιμοποιείτε το Jellyfin για Android! Συνιστάται πάντα η χρήση του πιο πρόσφατου διακομιστή Jellyfin.
+
+ Για περισσότερες πληροφορίες, ανατρέξτε στο blog μας στη διεύθυνση jellyfin.org, ή διαβάστε το πλήρες αρχείο αλλαγών στο GitHub.
diff --git a/fastlane/metadata/android/el/full_description.txt b/fastlane/metadata/android/el/full_description.txt
new file mode 100644
index 0000000..02124f5
--- /dev/null
+++ b/fastlane/metadata/android/el/full_description.txt
@@ -0,0 +1,14 @@
+Τα πολυμέσα σας, οι κανόνες σας.
+
+Το έργο Jellyfin είναι ένας δωρεάν εξυπηρέτης πολυμέσων ανοιχτού κώδικα. Χωρίς χρεώσεις, παρακολούθηση και κρυφή ατζέντα. Με τον δωρεάν εξυπηρέτη μας συγκεντρώνετε όλες τις φωτογραφίες σας και τα ηχητικά και οπτικοακουστικά ντοκουμέντα σε ένα μέρος.
+
+Η χρήση της εφαρμογής προϋποθέτει την εγκατάσταση του εξυπηρέτη. Μάθετε περισσότερα στο jellyfin.org.
+
+Με τον εξυπηρέτη Jellyfin μπορείτε να:
+
+* παρακολουθήσετε ζωντανή τηλεόραση και καταγεγραμμένες εκπομπές (προϋποθέτει κατάλληλο εξοπλισμό και υπηρεσίες)
+* εκπέμπετε ροές σε συσκευές Chromecast στο δίκτυό σας
+* εκπέμπετε ροές σε συσκευές πολυμέσων Android
+* δείτε τις συλλογές σας με εύχρηστες διεπαφές
+
+Αυτή είναι η επίσημη εφαρμογή για Android. Ευχαριστούμε που χρησιμοποιείτε το Jellyfin!
diff --git a/fastlane/metadata/android/el/short_description.txt b/fastlane/metadata/android/el/short_description.txt
new file mode 100644
index 0000000..f35fb2e
--- /dev/null
+++ b/fastlane/metadata/android/el/short_description.txt
@@ -0,0 +1 @@
+Διαμεσολαβητής αναπαραγωγής Jellyfin για το κινητό σας, το δωρεάν σύστημα πολυμέσων
diff --git a/fastlane/metadata/android/el/title.txt b/fastlane/metadata/android/el/title.txt
new file mode 100644
index 0000000..823b773
--- /dev/null
+++ b/fastlane/metadata/android/el/title.txt
@@ -0,0 +1 @@
+Jellyfin - τα αρχεία πολυμέσων σας, στα χέρια σας!
diff --git a/fastlane/metadata/android/en-US/changelogs/default.txt b/fastlane/metadata/android/en-US/changelogs/default.txt
new file mode 100644
index 0000000..cbcf9eb
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/default.txt
@@ -0,0 +1,3 @@
+Thank you for using Jellyfin for Android! Using the latest released Jellyfin Server is always recommended.
+
+For more information, please see our blog at jellyfin.org, or read the full changelog on GitHub.
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
new file mode 100644
index 0000000..e0b8c0d
--- /dev/null
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -0,0 +1,14 @@
+Your media, on your terms.
+
+The Jellyfin project is an open source, free software media server. No fees, no tracking, no hidden agenda. Get our free server to collect all your audio, video, photos, and more in one place.
+
+To use the app, you must have a Jellyfin server set up and running. Find out more at jellyfin.org.
+
+With a Jellyfin server, you can:
+
+* Watch Live TV and recorded shows from your Jellyfin server (additional hardware/services required)
+* Stream to a Chromecast device on your network
+* Stream your media to your Android device
+* View your collection in an easy to use interface
+
+This is the official Jellyfin companion app for Android. Thank you for using Jellyfin!
diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png
new file mode 100644
index 0000000..b7ebb91
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png
new file mode 100644
index 0000000..feb10f2
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/icon.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_1.png
new file mode 100644
index 0000000..df8b23a
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_1.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_2.png
new file mode 100644
index 0000000..6bfe1c4
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_2.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_3.png
new file mode 100644
index 0000000..1ba8b06
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_3.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_4.png
new file mode 100644
index 0000000..a3b2599
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_4.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_5.png
new file mode 100644
index 0000000..3f8d3f1
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_5.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_6.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_6.png
new file mode 100644
index 0000000..242f54f
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_6.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_7.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_7.png
new file mode 100644
index 0000000..13ecf21
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_7.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_8.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_8.png
new file mode 100644
index 0000000..fbd95c9
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/screenshot_phone_8.png differ
diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt
new file mode 100644
index 0000000..18231d4
--- /dev/null
+++ b/fastlane/metadata/android/en-US/short_description.txt
@@ -0,0 +1 @@
+Mobile client for Jellyfin, the free software media system
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt
new file mode 100644
index 0000000..2a9e5fc
--- /dev/null
+++ b/fastlane/metadata/android/en-US/title.txt
@@ -0,0 +1 @@
+Jellyfin - your media in your hands!
\ No newline at end of file
diff --git a/fastlane/metadata/android/eo/full_description.txt b/fastlane/metadata/android/eo/full_description.txt
new file mode 100644
index 0000000..9ffeaf0
--- /dev/null
+++ b/fastlane/metadata/android/eo/full_description.txt
@@ -0,0 +1,14 @@
+Viaj plurmedioj, laŭ viaj kondiĉoj.
+
+La Jellyfin estas malfermitkoda projekto, plurmedia servilo de libera programaro. Neniuj kotizoj, neniu sekvado, neniu kaŝita tagordo. Akiru nian senpagan servilon por kolekti ĉiujn viajn aŭdaĵojn, videojn, fotojn kaj pli en unu loko.
+
+Por uzi la apon, vi devas havi Jellyfin-servilon agorditan kaj funkciantan. Eksciu pli ĉe jellyfin.org.
+
+Kun Jellyfin-servilo vi povas:
+
+* Spekti TV-eteron kaj rikorditajn seriojn de via Jellyfin-servilo (aldonaj aparataro/servoj necesas)
+* Elsendflui al Chromecast-aparato en via reto
+* Elsendflui vian plurmediojn al via Android-aparato
+* Spekti vian kolektojn en faciluza interfaco
+
+Jen la oficiala akompana Jellyfin-apo por Android. Dankon pro uzi Jellyfin!
diff --git a/fastlane/metadata/android/eo/short_description.txt b/fastlane/metadata/android/eo/short_description.txt
new file mode 100644
index 0000000..1f7769e
--- /dev/null
+++ b/fastlane/metadata/android/eo/short_description.txt
@@ -0,0 +1 @@
+Poŝfona kliento por Jellyfin, la plurmedia sistemo de libera programaro
diff --git a/fastlane/metadata/android/eo/title.txt b/fastlane/metadata/android/eo/title.txt
new file mode 100644
index 0000000..6916915
--- /dev/null
+++ b/fastlane/metadata/android/eo/title.txt
@@ -0,0 +1 @@
+Jellyfin - ĉio en viaj manoj!
diff --git a/fastlane/metadata/android/es-MX/full_description.txt b/fastlane/metadata/android/es-MX/full_description.txt
new file mode 100644
index 0000000..86ab6e5
--- /dev/null
+++ b/fastlane/metadata/android/es-MX/full_description.txt
@@ -0,0 +1,14 @@
+Tu multimedia, en tus términos
+
+El proyecto Jellyfin es un servidor de multimedia gratuito y de código abierto. Sin cargos, sin rastrearte y sin intenciones ocultas. Obtén nuestro servidor gratuito para recopilar todo tu audio, video, fotos y más en un solo lugar.
+
+Para usar la aplicación, deberás tener un servidor Jellyfin configurado y corriendo. Aprende más en jellyfin.org
+
+Con un servidor Jellyfin podrás:
+
+* Mirar TV en vivo y grabar programas desde tu servidor Jellyfin (require hardware/servicios adicionales)
+* Transmitir hacia un dispositivo Chromecast en tu red
+* Consumir to multimedia a tu dispositivo Android
+* Ver tu coleccion en una interfaz fácil de usar
+
+Esta es la aplicacion complementaria oficial de Jellyfin para Android. Gracias por usar Jellyfin!
diff --git a/fastlane/metadata/android/es-MX/short_description.txt b/fastlane/metadata/android/es-MX/short_description.txt
new file mode 100644
index 0000000..d5beccf
--- /dev/null
+++ b/fastlane/metadata/android/es-MX/short_description.txt
@@ -0,0 +1 @@
+Cliente móvil para Jellyfin, El sistema multimedia de software gratuito
diff --git a/fastlane/metadata/android/es-MX/title.txt b/fastlane/metadata/android/es-MX/title.txt
new file mode 100644
index 0000000..6ad5b3f
--- /dev/null
+++ b/fastlane/metadata/android/es-MX/title.txt
@@ -0,0 +1 @@
+Jellyfin - ¡tus archivos multimedia en tus manos!
diff --git a/fastlane/metadata/android/es/changelogs/default.txt b/fastlane/metadata/android/es/changelogs/default.txt
new file mode 100644
index 0000000..f78ff01
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/default.txt
@@ -0,0 +1,3 @@
+¡Gracias por usar Jellyfin en Android TV! Siempre se recomienda utilizar el servidor Jellyfin más reciente.
+
+Para obtener más información, consulte nuestro blog en jellyfin.org o lea el registro de cambios completo en GitHub.
diff --git a/fastlane/metadata/android/es/full_description.txt b/fastlane/metadata/android/es/full_description.txt
new file mode 100644
index 0000000..542e9a1
--- /dev/null
+++ b/fastlane/metadata/android/es/full_description.txt
@@ -0,0 +1,14 @@
+Tu multimedia, en tus términos.
+
+El proyecto Jellyfin es un servidor multimedia de software libre y de código abierto. Sin tarifas, sin seguimiento, sin motivos ocultos. Obtenga nuestro servidor gratuito para recopilar todo su audio, video, fotos y más en un solo lugar.
+
+Para utilizar la aplicación, debe tener un servidor Jellyfin configurado y en funcionamiento. Obtenga más información en jellyfin.org .
+
+Con un servidor Jellyfin, puede:
+
+* Ver TV en vivo y programas grabados desde su servidor Jellyfin (se requiere hardware / servicios adicionales)
+* Transmitir a un dispositivo Chromecast en su red
+* Transmitir su multimedia a su dispositivo Android
+* Ver su colección en una interfaz fácil de usar
+
+Esta es la aplicación oficial de Jellyfin para Android. ¡Gracias por usar Jellyfin!
diff --git a/fastlane/metadata/android/es/short_description.txt b/fastlane/metadata/android/es/short_description.txt
new file mode 100644
index 0000000..c89bf6e
--- /dev/null
+++ b/fastlane/metadata/android/es/short_description.txt
@@ -0,0 +1 @@
+Cliente móvil para Jellyfin, el sistema multimedia de software libre
diff --git a/fastlane/metadata/android/es/title.txt b/fastlane/metadata/android/es/title.txt
new file mode 100644
index 0000000..43b1d1f
--- /dev/null
+++ b/fastlane/metadata/android/es/title.txt
@@ -0,0 +1 @@
+Jellyfin - ¡tu multimedia en tus manos!
diff --git a/fastlane/metadata/android/et/full_description.txt b/fastlane/metadata/android/et/full_description.txt
new file mode 100644
index 0000000..4854b47
--- /dev/null
+++ b/fastlane/metadata/android/et/full_description.txt
@@ -0,0 +1,14 @@
+Sinu meedia sinu tingimustel.
+
+Jellyfin projekt on avatud lähtekoodiga vaba tarkvara meediaserver. Pole teenustasu, pole jälgimist ega varjatud plaani. Hangi meie tasuta server, et koguda kogu oma muusika, video, fotod ja muu ühte kohta.
+
+Rakenduse kasutamiseks peab olema Jellyfini server häälestatud ja käivitatud. Lisateavet leiab aadressiltjellyfin.org.
+
+Jellyfini serveriga saad:
+
+* Vaadata TV otsesaateid ja salvestusi (nõuab lisavarustust/teenuseid)
+* Voogesitada Chromecast seadmega oma võrgus
+* Voogesitada oma meediat oma Android seadmesse
+* Sirvida oma kollektsiooni hõlpsasti kasutatava liidese abil
+
+See on Jellyfini ametlik rakendus Androidile. Täname sind Jellyfini kasutamise eest!
diff --git a/fastlane/metadata/android/et/short_description.txt b/fastlane/metadata/android/et/short_description.txt
new file mode 100644
index 0000000..9b726ab
--- /dev/null
+++ b/fastlane/metadata/android/et/short_description.txt
@@ -0,0 +1 @@
+Mobiiliklient Jellifynile, tasuta tarkvara meediasüsteemile
diff --git a/fastlane/metadata/android/et/title.txt b/fastlane/metadata/android/et/title.txt
new file mode 100644
index 0000000..dc8c758
--- /dev/null
+++ b/fastlane/metadata/android/et/title.txt
@@ -0,0 +1 @@
+Jellyfin - Sinu meedia sinu kätes!
diff --git a/fastlane/metadata/android/fa/full_description.txt b/fastlane/metadata/android/fa/full_description.txt
new file mode 100644
index 0000000..6c1a225
--- /dev/null
+++ b/fastlane/metadata/android/fa/full_description.txt
@@ -0,0 +1,14 @@
+با توجه به شرایط شما ، رسانه شما
+
+پروژه Jellyfin یک سرور رسانه ای نرم افزار رایگان و منبع باز است. بدون هزینه ، بدون ردیابی ، بدون دستور کار پنهان. سرور رایگان ما را برای جمع آوری همه صوتی ، تصویری ، عکسها و موارد دیگر در یک مکان دریافت کنید.
+
+برای استفاده از برنامه ، باید یک سرور Jellyfin راه اندازی و راه اندازی کنید. در jellyfin.org اطلاعات بیشتری کسب کنید.
+
+با یک سرور Jellyfin می توانید:
+
+* تماشای تلویزیون زنده و نمایش های ضبط شده از سرور Jellyfin خود (سخت افزار / خدمات اضافی مورد نیاز است)
+* به یک دستگاه Chromecast در شبکه خود جریان دهید
+* رسانه خود را به دستگاه Android خود منتقل کنید
+* مجموعه خود را در رابط کاربری آسان مشاهده کنید
+
+این برنامه رسمی همراه Jellyfin برای آندروید است. با تشکر از شما برای استفاده از Jellyfin!
diff --git a/fastlane/metadata/android/fa/short_description.txt b/fastlane/metadata/android/fa/short_description.txt
new file mode 100644
index 0000000..8706305
--- /dev/null
+++ b/fastlane/metadata/android/fa/short_description.txt
@@ -0,0 +1 @@
+سرویس گیرنده موبایل برای Jellyfin ، سیستم رسانه نرم افزار رایگان
diff --git a/fastlane/metadata/android/fa/title.txt b/fastlane/metadata/android/fa/title.txt
new file mode 100644
index 0000000..3d5e123
--- /dev/null
+++ b/fastlane/metadata/android/fa/title.txt
@@ -0,0 +1 @@
+Jellyfin - رسانه شما در دست شماست!
diff --git a/fastlane/metadata/android/fi/changelogs/default.txt b/fastlane/metadata/android/fi/changelogs/default.txt
new file mode 100644
index 0000000..49321b2
--- /dev/null
+++ b/fastlane/metadata/android/fi/changelogs/default.txt
@@ -0,0 +1,3 @@
+Kiitos kun käytät Jellyfiniä Androidilla! Uusimman Jellyfin-palvelinversion käyttäminen on aina suositeltavaa.
+
+Lisätietoja saat jellyfin.org-sivuston blogista ja täydellinen muutoshistoria löytyy GitHubista.
diff --git a/fastlane/metadata/android/fi/full_description.txt b/fastlane/metadata/android/fi/full_description.txt
new file mode 100644
index 0000000..3832690
--- /dev/null
+++ b/fastlane/metadata/android/fi/full_description.txt
@@ -0,0 +1,14 @@
+Oma mediasi, omilla ehdoillasi.
+
+Jellyfin-projekti on avoimen lähdekoodin vapaa mediapalvelinohjelmisto. Ei kuluja, ei seurantaa, ei piilotettua yllätyksiä. Hanki ilmainen palvelinsovellus ja kerää kaikki ääni-, video- ja kuvatiedostosi samaan paikkaan.
+
+Sovelluksen käyttö edellyttää Jellyfin-palvelimen asennusta ja suoritusta. Lisätietoja osoitteesta jellyfin.org.
+
+Jellyfin-palvelimella voit:
+
+* Katsoa suoria televisiolähetyksiä ja -tallenteita (vaatii lisälaitteita/-palveluita)
+* Suoratoistaa lähiverkon Chromecast-laitteisiin
+* Suoratoistaa Android-laitteisiin
+* Selata kokoelmaa helppokäyttöisessä käyttöliittymässä
+
+Tämä on virallinen Jellyfin-sovellus Androidille. Kiitos, kun käytät Jellyfiniä!
diff --git a/fastlane/metadata/android/fi/short_description.txt b/fastlane/metadata/android/fi/short_description.txt
new file mode 100644
index 0000000..f9f3ad6
--- /dev/null
+++ b/fastlane/metadata/android/fi/short_description.txt
@@ -0,0 +1 @@
+Mobiilisovellus ilmaiselle Jellyfin-mediajärjestelmälle
diff --git a/fastlane/metadata/android/fi/title.txt b/fastlane/metadata/android/fi/title.txt
new file mode 100644
index 0000000..5d42d44
--- /dev/null
+++ b/fastlane/metadata/android/fi/title.txt
@@ -0,0 +1 @@
+Jellyfin - media käsissäsi!
diff --git a/fastlane/metadata/android/fil/changelogs/default.txt b/fastlane/metadata/android/fil/changelogs/default.txt
new file mode 100644
index 0000000..af48446
--- /dev/null
+++ b/fastlane/metadata/android/fil/changelogs/default.txt
@@ -0,0 +1,3 @@
+Salamat sa paggamit ng Jellyfin para sa Android! Ang paggamit ng pinakabagong labas na Jellyfin Server ay parating nirerekomendema.
+
+Para sa karagdagang impormasyon, pakiusap at tingnan ang aming blog sa jellyfin.org, o kaya'y basahin ang kabouhang changelog sa GitHub.
diff --git a/fastlane/metadata/android/fil/full_description.txt b/fastlane/metadata/android/fil/full_description.txt
new file mode 100644
index 0000000..c09cbc4
--- /dev/null
+++ b/fastlane/metadata/android/fil/full_description.txt
@@ -0,0 +1,14 @@
+Media mo, hawak mo.
+
+Ang proyektong Jellyfin ay isang open source, at libreng software media server. Na walang bayad, walang pagsusubaybay, at walang nakatagong adyenda. Kuning ang aming libreng server upang maikolekta ang lahat ng iyong odyo, bidyo, larawan at iba pa sa iisang lugar.
+
+Upang magamit ang app, ikaw dapat ay may Jellyfin server na naisetup at gumagana. Alamin ang iba pa sa jellyfin.org.
+
+Sa Jellyfin server, kaya mong:
+
+* Manood ng Live TV at mga rekorded na talastas galing sa iyong Jellyfin server (karagdagang hardware/mga serbisyo ang kailangan)
+* Pag iistream papunta sa isang Chromecast device na nasa iyong kabalagan
+* Pag iistream ng iyong mga media papunta sa iyong Android device
+* Tingan ang iyong koleksyon sa isang madaling gamitin na interface.
+
+Ito ang opisyal na Jellyfin companion app para sa Android. Salamat sa iyong paggmit ng Jellyfin!
diff --git a/fastlane/metadata/android/fil/short_description.txt b/fastlane/metadata/android/fil/short_description.txt
new file mode 100644
index 0000000..cfbd567
--- /dev/null
+++ b/fastlane/metadata/android/fil/short_description.txt
@@ -0,0 +1 @@
+Mobile client para sa Jellyfin, ang libreng software media system
diff --git a/fastlane/metadata/android/fil/title.txt b/fastlane/metadata/android/fil/title.txt
new file mode 100644
index 0000000..7b61bff
--- /dev/null
+++ b/fastlane/metadata/android/fil/title.txt
@@ -0,0 +1 @@
+Jellyfin - media mo nasa kamay mo!
diff --git a/fastlane/metadata/android/fr/changelogs/default.txt b/fastlane/metadata/android/fr/changelogs/default.txt
new file mode 100644
index 0000000..6963196
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/default.txt
@@ -0,0 +1,3 @@
+Merci d'utiliser Jellyfin pour Android ! Utiliser la dernière version du serveur Jellyfin est toujours recommandé.
+
+Pour plus d'informations, veuillez visiter notre blog à l'adresse jellyfin.org ou lire la liste complètes des changements sur GitHub.
diff --git a/fastlane/metadata/android/fr/full_description.txt b/fastlane/metadata/android/fr/full_description.txt
new file mode 100644
index 0000000..e6b2e62
--- /dev/null
+++ b/fastlane/metadata/android/fr/full_description.txt
@@ -0,0 +1,14 @@
+Vos médias, comme vous le souhaitez.
+
+Jellyfin est un serveur multimédia libre et open-source. Pas de frais, pas de pistage et pas d'agenda caché. Procurez-vous notre serveur gratuit pour centraliser tous vos fichiers audio, vidéos, photos et autres.
+
+Pour utiliser cette application, vous devez au préalable installer et configurer un serveur Jellyfin. Pour en savoir plus, consultez jellyfin.org.
+
+Avec un serveur Jellyfin, vous pouvez :
+
+* Regarder et enregistrer vos émissions de télévision depuis votre serveur (matériel et/ou services supplémentaire(s) requis)
+* Diffuser vers un appareil Chromecast sur votre réseau
+* Diffuser vos médias sur votre appareil Android
+* Consulter votre médiathèque via une interface intuitive
+
+Ceci est l'application compagnon officielle de Jellyfin pour Android. Merci d'utiliser Jellyfin !
diff --git a/fastlane/metadata/android/fr/short_description.txt b/fastlane/metadata/android/fr/short_description.txt
new file mode 100644
index 0000000..4955c40
--- /dev/null
+++ b/fastlane/metadata/android/fr/short_description.txt
@@ -0,0 +1 @@
+Client mobile pour Jellyfin, le système multimédia libre
diff --git a/fastlane/metadata/android/fr/title.txt b/fastlane/metadata/android/fr/title.txt
new file mode 100644
index 0000000..38f3005
--- /dev/null
+++ b/fastlane/metadata/android/fr/title.txt
@@ -0,0 +1 @@
+Jellyfin - Vos médias en main !
diff --git a/fastlane/metadata/android/he-IL/changelogs/default.txt b/fastlane/metadata/android/he-IL/changelogs/default.txt
new file mode 100644
index 0000000..0cd2f24
--- /dev/null
+++ b/fastlane/metadata/android/he-IL/changelogs/default.txt
@@ -0,0 +1,3 @@
+תודה לך על השימוש ב־Jellyfin ל-Android! השימוש בגרסה העדכנית ביותר של שרת Jellyfin הוא תמיד מומלץ.
+
+למידע נוסף ניתן לבקר בבלוג שלנו בכתובת jellyfin.org או לעיין בתיעוד השינויים ב־GitHub.
diff --git a/fastlane/metadata/android/he-IL/full_description.txt b/fastlane/metadata/android/he-IL/full_description.txt
new file mode 100644
index 0000000..1246c52
--- /dev/null
+++ b/fastlane/metadata/android/he-IL/full_description.txt
@@ -0,0 +1,14 @@
+התקשורת שלך, בתנאים שלך.
+
+פרויקט Jellyfin הוא שרת מדיה תוכנה חופשית בקוד פתוח. ללא עמלות, ללא מעקב, ללא כוונה נסתרת. קבל את השרת החינמי שלנו כדי לרכז את כל האודיו, הווידאו, התמונות ועוד במקום אחד.
+
+כדי להשתמש באפליקציה, עליך להגדיר ולהפעיל שרת Jellyfin. למידע נוסף ב-jellyfin.org.
+
+עם שרת Jellyfin, אתה יכול:
+
+* צפה בטלוויזיה בשידור חי ובתוכניות מוקלטות משרת הג'ליפין שלך (נדרש חומרה/שירותים נוספים)
+* הזרם למכשיר Chromecast ברשת שלך
+* הזרמת המדיה שלך למכשיר האנדרואיד שלך
+* הצג את האוסף שלך בממשק קל לשימוש
+
+זוהי האפליקציה הרשמית הנלווית של Jellyfin עבור Android TV. תודה על השימוש Jellyfin!
diff --git a/fastlane/metadata/android/he-IL/short_description.txt b/fastlane/metadata/android/he-IL/short_description.txt
new file mode 100644
index 0000000..b0f7bdd
--- /dev/null
+++ b/fastlane/metadata/android/he-IL/short_description.txt
@@ -0,0 +1 @@
+קליינט Jellyfin למובייל, תוכנה חינמית למערכות מדיה
diff --git a/fastlane/metadata/android/he-IL/title.txt b/fastlane/metadata/android/he-IL/title.txt
new file mode 100644
index 0000000..57f3010
--- /dev/null
+++ b/fastlane/metadata/android/he-IL/title.txt
@@ -0,0 +1 @@
+Jellyfin - המדיה שלך בידיך!
diff --git a/fastlane/metadata/android/he/changelogs/default.txt b/fastlane/metadata/android/he/changelogs/default.txt
new file mode 100644
index 0000000..0cd2f24
--- /dev/null
+++ b/fastlane/metadata/android/he/changelogs/default.txt
@@ -0,0 +1,3 @@
+תודה לך על השימוש ב־Jellyfin ל-Android! השימוש בגרסה העדכנית ביותר של שרת Jellyfin הוא תמיד מומלץ.
+
+למידע נוסף ניתן לבקר בבלוג שלנו בכתובת jellyfin.org או לעיין בתיעוד השינויים ב־GitHub.
diff --git a/fastlane/metadata/android/he/full_description.txt b/fastlane/metadata/android/he/full_description.txt
new file mode 100644
index 0000000..1246c52
--- /dev/null
+++ b/fastlane/metadata/android/he/full_description.txt
@@ -0,0 +1,14 @@
+התקשורת שלך, בתנאים שלך.
+
+פרויקט Jellyfin הוא שרת מדיה תוכנה חופשית בקוד פתוח. ללא עמלות, ללא מעקב, ללא כוונה נסתרת. קבל את השרת החינמי שלנו כדי לרכז את כל האודיו, הווידאו, התמונות ועוד במקום אחד.
+
+כדי להשתמש באפליקציה, עליך להגדיר ולהפעיל שרת Jellyfin. למידע נוסף ב-jellyfin.org.
+
+עם שרת Jellyfin, אתה יכול:
+
+* צפה בטלוויזיה בשידור חי ובתוכניות מוקלטות משרת הג'ליפין שלך (נדרש חומרה/שירותים נוספים)
+* הזרם למכשיר Chromecast ברשת שלך
+* הזרמת המדיה שלך למכשיר האנדרואיד שלך
+* הצג את האוסף שלך בממשק קל לשימוש
+
+זוהי האפליקציה הרשמית הנלווית של Jellyfin עבור Android TV. תודה על השימוש Jellyfin!
diff --git a/fastlane/metadata/android/he/short_description.txt b/fastlane/metadata/android/he/short_description.txt
new file mode 100644
index 0000000..b0f7bdd
--- /dev/null
+++ b/fastlane/metadata/android/he/short_description.txt
@@ -0,0 +1 @@
+קליינט Jellyfin למובייל, תוכנה חינמית למערכות מדיה
diff --git a/fastlane/metadata/android/he/title.txt b/fastlane/metadata/android/he/title.txt
new file mode 100644
index 0000000..57f3010
--- /dev/null
+++ b/fastlane/metadata/android/he/title.txt
@@ -0,0 +1 @@
+Jellyfin - המדיה שלך בידיך!
diff --git a/fastlane/metadata/android/hi/changelogs/default.txt b/fastlane/metadata/android/hi/changelogs/default.txt
new file mode 100644
index 0000000..f55ed74
--- /dev/null
+++ b/fastlane/metadata/android/hi/changelogs/default.txt
@@ -0,0 +1,3 @@
+एंड्रॉइड के लिए जेलीफिन का उपयोग करने के लिए धन्यवाद! नवीनतम जारी जेलीफिन सर्वर का उपयोग करने की हमेशा सिफारिश की जाती है।
+
+अधिक जानकारी के लिए, कृपया हमारे ब्लॉग को jellyfin.org पर देखें, या गिटहब पर पूरा चेंजलॉग पढ़ें।
diff --git a/fastlane/metadata/android/hi/full_description.txt b/fastlane/metadata/android/hi/full_description.txt
new file mode 100644
index 0000000..1d87cbb
--- /dev/null
+++ b/fastlane/metadata/android/hi/full_description.txt
@@ -0,0 +1,14 @@
+आपका मीडिया, आपकी शर्तों पर।
+
+जेलीफिन प्रोजेक्ट एक ओपन सोर्स, फ्री सॉफ्टवेयर मीडिया सर्वर है। कोई फीस नहीं, कोई ट्रैकिंग नहीं, कोई छिपा हुआ एजेंडा नहीं। अपने सभी ऑडियो, वीडियो, फ़ोटो, और बहुत कुछ एक ही स्थान पर एकत्रित करने के लिए हमारा निःशुल्क सर्वर प्राप्त करें।
+
+ऐप का उपयोग करने के लिए, आपके पास जेलीफिन सर्वर स्थापित और चालू होना चाहिए। jellyfin.org पर और जानें।
+
+जेलीफिन सर्वर के साथ, आप कर सकते हैं:
+
+* अपने जेलीफिन सर्वर से लाइव टीवी और रिकॉर्ड किए गए शो देखें (अतिरिक्त हार्डवेयर/सेवाओं की आवश्यकता)
+* अपने नेटवर्क पर क्रोमकास्ट डिवाइस पर स्ट्रीम करें
+* अपने मीडिया को अपने एंड्रॉइड डिवाइस पर स्ट्रीम करें
+* अपने संग्रह को आसान तरीके से देखें
+
+इंटरफ़ेस का उपयोग करने के लिए यह एंड्रॉइड के लिए आधिकारिक जेलीफिन सहयोगी ऐप है। जेलीफिन का उपयोग करने के लिए धन्यवाद!
diff --git a/fastlane/metadata/android/hi/short_description.txt b/fastlane/metadata/android/hi/short_description.txt
new file mode 100644
index 0000000..e08706d
--- /dev/null
+++ b/fastlane/metadata/android/hi/short_description.txt
@@ -0,0 +1 @@
+जेलीफिन का मोबाइल क्लाइंट , निःशुल्क मीडिया सॉफ्टवेयर
diff --git a/fastlane/metadata/android/hi/title.txt b/fastlane/metadata/android/hi/title.txt
new file mode 100644
index 0000000..916a66a
--- /dev/null
+++ b/fastlane/metadata/android/hi/title.txt
@@ -0,0 +1 @@
+जेलीफिन - आपकी मीडिया आपकी हाथ में !
diff --git a/fastlane/metadata/android/hr/changelogs/default.txt b/fastlane/metadata/android/hr/changelogs/default.txt
new file mode 100644
index 0000000..ab56074
--- /dev/null
+++ b/fastlane/metadata/android/hr/changelogs/default.txt
@@ -0,0 +1,3 @@
+Hvala što koristite Jellyfin za Android! Uvijek se preporučuje korištenje najnovijeg izdanja Jellyfin poslužitelja.
+
+Za više informacija pogledajte naš blog na jellyfin.org ili pročitajte cijeli dnevnik promjena na GitHubu.
diff --git a/fastlane/metadata/android/hr/full_description.txt b/fastlane/metadata/android/hr/full_description.txt
new file mode 100644
index 0000000..919a53d
--- /dev/null
+++ b/fastlane/metadata/android/hr/full_description.txt
@@ -0,0 +1,14 @@
+Vaši medijski sadržaji, pod vašim uvjetima.
+
+Projekt Jellyfin je medijski poslužitelj besplatnog aplikacija otvorenog koda. Bez naknada, bez praćenja, bez skrivenog plana. Nabavite naš besplatni poslužitelj za prikupljanje svih vaših audio zapisa, video zapisa, fotografija i ostalog na jednom mjestu.
+
+Za korištenja aplikacije morate imati Jellyfin poslužitelj postavljen i pokrenut. Saznajte više na jellyfin.org.
+
+S Jellyfin poslužiteljem možete:
+
+* Gledati TV uživo i snimljene emisije s vašeg Jellyfin poslužitelja (potreban je dodatni hardver/usluge)
+* Strujanje na Chromecast uređaj na vašoj mreži
+* Strujanje svojih medijskih sadržaja na svoj Android uređaj
+* Pregledajte svoju zbirku u sučelju jednostavnom za korištenje
+
+Ovo je službena Jellyfin popratna aplikacija za Android. Hvala što koristite Jellyfin!
diff --git a/fastlane/metadata/android/hr/short_description.txt b/fastlane/metadata/android/hr/short_description.txt
new file mode 100644
index 0000000..9a8351f
--- /dev/null
+++ b/fastlane/metadata/android/hr/short_description.txt
@@ -0,0 +1 @@
+Mobilni klijent za Jellyfin, besplatna aplikacija za medijski sustav
diff --git a/fastlane/metadata/android/hr/title.txt b/fastlane/metadata/android/hr/title.txt
new file mode 100644
index 0000000..adce8b2
--- /dev/null
+++ b/fastlane/metadata/android/hr/title.txt
@@ -0,0 +1 @@
+Jellyfin - vaši mediji u Vašim rukama!
diff --git a/fastlane/metadata/android/hu/changelogs/default.txt b/fastlane/metadata/android/hu/changelogs/default.txt
new file mode 100644
index 0000000..0865cf2
--- /dev/null
+++ b/fastlane/metadata/android/hu/changelogs/default.txt
@@ -0,0 +1,3 @@
+Köszönjük, hogy a Jellyfin Androidot használja! Mindig a legújabb kiadású Jellyfin Szerver használata ajánlott.
+
+További információkért kérjük, olvassa el a blogunkat a jellyfin.org oldalon, vagy a teljes changelogot a GitHubon.
diff --git a/fastlane/metadata/android/hu/full_description.txt b/fastlane/metadata/android/hu/full_description.txt
new file mode 100644
index 0000000..8f2bc5b
--- /dev/null
+++ b/fastlane/metadata/android/hu/full_description.txt
@@ -0,0 +1,14 @@
+A te médiád, a te feltételeid szerint.
+
+A Jellyfin projekt egy nyílt forráskódú, ingyenesen elérhető szoftver. Nincsenek díjak, nincs nyomonkövetés, nincsenek rejtett, később felmerülő zavaró tényezők . Szerezd be az ingyenes médiaszerveredet, és gyűjtsd egy helyen a zenédet, videóidat és képeidet!
+
+Az alkalmazáshoz szükséges egy beállított és működő Jellyfin szerver. További információkat a jellyfin.org webhelyen találhatsz.
+
+A Jellyfin szerver főbb funkciói:
+
+* Élő TV-nézés, felvételek készítése (további hardver/szolgáltatás szükséges)
+* Streamelj Chromecast eszközre a helyi hálózatodon.
+* Média streamelése az Android eszközeidre.
+* A gyűjteményed megtekintése egy jól használható, felhasználóbarát felületen.
+
+Ez az alkalmazás a Jellyfin hivatalos alkalmazása. Köszönjük, hogy a Jellyfint választottad!
diff --git a/fastlane/metadata/android/hu/short_description.txt b/fastlane/metadata/android/hu/short_description.txt
new file mode 100644
index 0000000..1e8a070
--- /dev/null
+++ b/fastlane/metadata/android/hu/short_description.txt
@@ -0,0 +1 @@
+Mobil kliens a Jellyfinhez, az ingyenes médiarendszer
diff --git a/fastlane/metadata/android/hu/title.txt b/fastlane/metadata/android/hu/title.txt
new file mode 100644
index 0000000..77271d8
--- /dev/null
+++ b/fastlane/metadata/android/hu/title.txt
@@ -0,0 +1 @@
+Jellyfin - kezedben a média!
diff --git a/fastlane/metadata/android/id/changelogs/default.txt b/fastlane/metadata/android/id/changelogs/default.txt
new file mode 100644
index 0000000..53ad31b
--- /dev/null
+++ b/fastlane/metadata/android/id/changelogs/default.txt
@@ -0,0 +1,3 @@
+Terima kasih telah menggunakan Jellyfin untuk Android! Menggunakan Jellyfin Server yang dirilis terbaru selalu disarankan.
+
+Untuk informasi lebih lanjut, silakan lihat blog kami di jellyfin.org, atau baca log perubahan lengkap di GitHub.
diff --git a/fastlane/metadata/android/id/full_description.txt b/fastlane/metadata/android/id/full_description.txt
new file mode 100644
index 0000000..97ea894
--- /dev/null
+++ b/fastlane/metadata/android/id/full_description.txt
@@ -0,0 +1,14 @@
+Media Anda, dengan persyaratan Anda.
+
+Proyek Jellyfin adalah open source, server media perangkat lunak gratis. Tanpa biaya, tanpa pelacakan, tanpa agenda tersembunyi. Dapatkan server gratis kami untuk mengumpulkan semua audio, video, foto, dan lainnya di satu tempat.
+
+Untuk menggunakan aplikasi ini, Anda harus menyiapkan dan menjalankan server Jellyfin. Cari tahu lebih lanjut di jellyfin.org.
+
+Dengan server Jellyfin, Anda dapat:
+
+* Tonton TV Langsung dan rekaman acara dari server Jellyfin Anda (diperlukan perangkat keras/layanan tambahan)
+* Streaming ke perangkat Chromecast di jaringan Anda
+* Streaming media Anda ke perangkat Android Anda
+* Lihat koleksi Anda dengan antarmuka yang mudah digunakan
+
+Ini adalah aplikasi pendamping Jellyfin resmi untuk Android. Terima kasih telah menggunakan Jellyfin!
diff --git a/fastlane/metadata/android/id/short_description.txt b/fastlane/metadata/android/id/short_description.txt
new file mode 100644
index 0000000..6084198
--- /dev/null
+++ b/fastlane/metadata/android/id/short_description.txt
@@ -0,0 +1 @@
+Klien seluler untuk Jellyfin, sistem media perangkat lunak gratis
diff --git a/fastlane/metadata/android/id/title.txt b/fastlane/metadata/android/id/title.txt
new file mode 100644
index 0000000..7ab1e41
--- /dev/null
+++ b/fastlane/metadata/android/id/title.txt
@@ -0,0 +1 @@
+Jellyfin - media di genggaman Anda!
diff --git a/fastlane/metadata/android/it/changelogs/default.txt b/fastlane/metadata/android/it/changelogs/default.txt
new file mode 100644
index 0000000..3786f20
--- /dev/null
+++ b/fastlane/metadata/android/it/changelogs/default.txt
@@ -0,0 +1,3 @@
+Grazie per aver scelto Jellyfin sulla tua Android TV! Ti raccomandiamo di usare l'ultima versione del Server Jellyfin.
+
+Per ulteriori informazioni, visita il nostro blog al sito jellyfin.org, o leggi la lista completa di modifiche su GitHub.
diff --git a/fastlane/metadata/android/it/full_description.txt b/fastlane/metadata/android/it/full_description.txt
new file mode 100644
index 0000000..60526c3
--- /dev/null
+++ b/fastlane/metadata/android/it/full_description.txt
@@ -0,0 +1,14 @@
+I tuoi media, le tue condizioni.
+
+Il progetto Jellyfin è un server multimediale open source e gratuito. Niente tariffe, nessun tracciamento, nessuna sorpresa. Ottieni il tuo server gratuito e raccogli tutti i tuoi audio, video, foto e molto altro in un unico posto.
+
+Per usare l'app, devi essere in possesso di un server Jellyfin funzionante. Scopri altri dettagli su jellyfin.org.
+
+Con un server Jellyfin potrai:
+
+* Guardare Live TV e programmi registrati direttamente dal tuo server Jellyfin (necessari hardware/servizi ausiliari)
+* Condividere su una Chromecast nel tuo network
+* Vedere i tuoi media sui tuoi dispositivi Android
+* Visualizzare le tue collezioni in un'interfaccia semplice ed intuitiva
+
+Questa è l'app ufficiale Jellyfin per Android. Grazie per utilizzare Jellyfin!
diff --git a/fastlane/metadata/android/it/short_description.txt b/fastlane/metadata/android/it/short_description.txt
new file mode 100644
index 0000000..d7c6833
--- /dev/null
+++ b/fastlane/metadata/android/it/short_description.txt
@@ -0,0 +1 @@
+Client mobile per Jellyfin, il sistema multimediale gratuito
diff --git a/fastlane/metadata/android/it/title.txt b/fastlane/metadata/android/it/title.txt
new file mode 100644
index 0000000..5c50f94
--- /dev/null
+++ b/fastlane/metadata/android/it/title.txt
@@ -0,0 +1 @@
+Jellyfin - I tuoi contenuti, nelle tue mani!
diff --git a/fastlane/metadata/android/ja/changelogs/default.txt b/fastlane/metadata/android/ja/changelogs/default.txt
new file mode 100644
index 0000000..3c2adba
--- /dev/null
+++ b/fastlane/metadata/android/ja/changelogs/default.txt
@@ -0,0 +1,3 @@
+Android TV で Jellyfin をご利用いただきありがとうございます!常に最新リリースの Jellyfin Server の使用をお勧めします。
+
+詳細については、jellyfin.org のブログをご覧いただくか、GitHub で完全な変更ログをお読みください。
diff --git a/fastlane/metadata/android/kk/full_description.txt b/fastlane/metadata/android/kk/full_description.txt
new file mode 100644
index 0000000..2f8b083
--- /dev/null
+++ b/fastlane/metadata/android/kk/full_description.txt
@@ -0,0 +1,14 @@
+Sızdıñ tasyğyşderekterıñız, öz şarttaryñyz boiynşa.
+
+Jellyfın jobasy - būl aşyq kodyndağy azat bağdarlamalyq jasaqtama negızındegı tasyğyşhana serverı. Eşqandai aqysyz, eşqandai qadağalausyz, eşqandai jasyryn josparsyz. Özıñızdıñ barlyq dybystardy, beinelerdı, fotosuretterdı jäne tağy basqalaryn bır jerde jinauğa mümkındık beretın tegın serverımızdı alyñyz.
+
+Qoldanbany paidalanu üşın sızde Jellyfın-server ornatylyp jäne jūmys ıstep tūruy kerek. Ony jellyfin.org saitynda köbırek bılıñız.
+
+Jellyfın-serverımen sız:
+
+* Jellyfın-serverınen efirlık TD men jazylğan körsetımderdı qaraisyz (qosymşa jabdyq / qyzmetter qajet);
+* Jelıñızdegı Chromecast-qūrylğysyna ağyn taratasyz;
+* Öz tasyğyşderekterıñızdı Androıd-qūrylğysyna jıberesız;
+* Öz jiyntyqtañyzdy qoldanuğa yñğaily interfeiste qarap şyğasyz.
+
+Būl Androıd üşın Jellyfın resmi tūtynğyştyq qoldanbasy. Jellyfın qoldanğanyñyzğa alğys!
diff --git a/fastlane/metadata/android/kk/short_description.txt b/fastlane/metadata/android/kk/short_description.txt
new file mode 100644
index 0000000..e23213d
--- /dev/null
+++ b/fastlane/metadata/android/kk/short_description.txt
@@ -0,0 +1 @@
+Jellyfin azat bağdarlamalyq tasyğyş jüiesı üşın ūialy tūtynğyş
diff --git a/fastlane/metadata/android/kk/title.txt b/fastlane/metadata/android/kk/title.txt
new file mode 100644
index 0000000..0dbd9e1
--- /dev/null
+++ b/fastlane/metadata/android/kk/title.txt
@@ -0,0 +1 @@
+Jellyfin — bärı öz qolyñyzda!
diff --git a/fastlane/metadata/android/lt/changelogs/default.txt b/fastlane/metadata/android/lt/changelogs/default.txt
new file mode 100644
index 0000000..1f0d838
--- /dev/null
+++ b/fastlane/metadata/android/lt/changelogs/default.txt
@@ -0,0 +1,3 @@
+Ačiū kad naudojatės Jellyfin skirtą Android! Visada rekomenduojame naudoti naujausią Jellyfin serverio versiją.
+
+Daugiau informacijos galite rasti mūsų bloge jellyfin.org arba perskaityti visą pakeitimų istoriją mūsų GitHub.
diff --git a/fastlane/metadata/android/lt/full_description.txt b/fastlane/metadata/android/lt/full_description.txt
new file mode 100644
index 0000000..90c5bb3
--- /dev/null
+++ b/fastlane/metadata/android/lt/full_description.txt
@@ -0,0 +1,14 @@
+Tavo turinys, su tavo taisyklėm.
+
+Jellyfin projektas yra atviro kodo, nemokamas turinio serveris. Jokių mokesčių, sekimo ar paslėptos agendos. Paleisk mūsų nemokamą serverį kad galėtum groti visą savo muziką, vaizdus, nuotraukas ir dar daugiau vienoje vietoje.
+
+Kad naudotis aplikacija, tu turi turėti Jellyfin serverį. Sužinok daugiau čia: jellyfin.org.
+
+Su Jellyfin serveriu tu gali:
+
+* Žiūrėti tiesiogines transliacijas ir įrašytas laidas iš savo Jellyfin serverio (reikalinga papildoma aparatinė ar programinė įranga)
+* Siųsti srautu į Chromecast įrenginį savo tinkle
+* Siųsti srautu savo turinį į savo Android įrenginį
+* Peržiūrėti savo kolekciją su paprasta naudoti vartotojo sąsasaja.
+
+Tai yra oficiali Jellyfin aplikacija skirta Android. Ačiū kad naudojatės Jellyfin!
diff --git a/fastlane/metadata/android/lt/short_description.txt b/fastlane/metadata/android/lt/short_description.txt
new file mode 100644
index 0000000..cd14fbe
--- /dev/null
+++ b/fastlane/metadata/android/lt/short_description.txt
@@ -0,0 +1 @@
+Mobili aplikacija skirta Jellyfin, nemokamai medijos sistemai
diff --git a/fastlane/metadata/android/lt/title.txt b/fastlane/metadata/android/lt/title.txt
new file mode 100644
index 0000000..f6d1991
--- /dev/null
+++ b/fastlane/metadata/android/lt/title.txt
@@ -0,0 +1 @@
+Jellyfin - turinys tavo rankose!
diff --git a/fastlane/metadata/android/mk-MK/changelogs/default.txt b/fastlane/metadata/android/mk-MK/changelogs/default.txt
new file mode 100644
index 0000000..6a4e7dc
--- /dev/null
+++ b/fastlane/metadata/android/mk-MK/changelogs/default.txt
@@ -0,0 +1,3 @@
+Ви благодариме што користите Jellyfin на Андроид! Секогаш се препорачува користење на најновиот објавен Jellyfin сервер.
+
+За повеќе информации, ве молиме погледнете го нашиот блог на jellyfin.org или прочитајте го целосниот дневник за промени на GitHub.
diff --git a/fastlane/metadata/android/mk-MK/full_description.txt b/fastlane/metadata/android/mk-MK/full_description.txt
new file mode 100644
index 0000000..3f50e48
--- /dev/null
+++ b/fastlane/metadata/android/mk-MK/full_description.txt
@@ -0,0 +1,14 @@
+Вашата медиумска содржина, по ваши услови.
+
+Проектот Jellyfin е медиумски сервер, слободен софтвер со отворен код. Бесплатен, без следење, без скриени намери. Добијте го нашиот бесплатен сервер за да ги соберете сите ваши песни, филмови, серии, фотографии и повеќе на едно место.
+
+За да ја користите апликацијата, мора да имате поставено свој сервер Jellyfin. Дознајте повеќе на jellyfin.org.
+
+Со Jellyfin серверот, го можете следното:
+
+* Да гледате ТВ во живо и снимани содржани од вашиот Jellyfin сервер (додатен хардвер/услуги се неопходни)
+* Стриминг на уред Chromecast на вашата мрежа
+* Стриминг на вашите медиумски содржини на вашиот Android уред
+* Преглед ја вашата колекција во интерфејс кој е лесен за употреба
+
+Ова е официјалната придружна апликација Jellyfin за Android. Ви благодариме што го користите Jellyfin!
diff --git a/fastlane/metadata/android/mk-MK/short_description.txt b/fastlane/metadata/android/mk-MK/short_description.txt
new file mode 100644
index 0000000..8b29101
--- /dev/null
+++ b/fastlane/metadata/android/mk-MK/short_description.txt
@@ -0,0 +1 @@
+Мобилен клиент за Jellyfin, бесплатен мултимедијален систем
diff --git a/fastlane/metadata/android/mk-MK/title.txt b/fastlane/metadata/android/mk-MK/title.txt
new file mode 100644
index 0000000..c1884eb
--- /dev/null
+++ b/fastlane/metadata/android/mk-MK/title.txt
@@ -0,0 +1 @@
+Jellyfin - вашата медиа во ваши раце!
diff --git a/fastlane/metadata/android/ml/full_description.txt b/fastlane/metadata/android/ml/full_description.txt
new file mode 100644
index 0000000..b1c8189
--- /dev/null
+++ b/fastlane/metadata/android/ml/full_description.txt
@@ -0,0 +1,14 @@
+നിങ്ങളുടെ നിബന്ധനകൾക്ക് വിധേയമായി നിങ്ങളുടെ മീഡിയ.
+
+ഒരു ഓപ്പൺ സോഴ്സ്, സ software ജന്യ സോഫ്റ്റ്വെയർ മീഡിയ സെർവറാണ് ജെല്ലിഫിൻ പ്രോജക്റ്റ്. ഫീസില്ല, ട്രാക്കിംഗ് ഇല്ല, മറഞ്ഞിരിക്കുന്ന അജണ്ടയില്ല. നിങ്ങളുടെ എല്ലാ ഓഡിയോ, വീഡിയോ, ഫോട്ടോകളും അതിലേറെയും ഒരിടത്ത് ശേഖരിക്കാൻ ഞങ്ങളുടെ സ server ജന്യ സെർവർ നേടുക.
+
+അപ്ലിക്കേഷൻ ഉപയോഗിക്കുന്നതിന്, നിങ്ങൾക്ക് ഒരു ജെല്ലിഫിൻ സെർവർ സജ്ജമാക്കി പ്രവർത്തിക്കണം. jellyfin.org ൽ കൂടുതൽ കണ്ടെത്തുക.
+
+ഒരു ജെല്ലിഫിൻ സെർവർ ഉപയോഗിച്ച്, നിങ്ങൾക്ക് ഇവ ചെയ്യാനാകും:
+
+* നിങ്ങളുടെ ജെല്ലിഫിൻ സെർവറിൽ നിന്ന് തത്സമയ ടിവിയും റെക്കോർഡുചെയ്ത ഷോകളും കാണുക (അധിക ഹാർഡ്വെയർ / സേവനങ്ങൾ ആവശ്യമാണ്)
+* നിങ്ങളുടെ നെറ്റ്വർക്കിലെ ഒരു Chromecast ഉപകരണത്തിലേക്ക് സ്ട്രീം ചെയ്യുക
+* നിങ്ങളുടെ Android ഉപകരണത്തിലേക്ക് മീഡിയ സ്ട്രീം ചെയ്യുക
+* ഉപയോഗിക്കാൻ എളുപ്പമുള്ള ഇന്റർഫേസിൽ നിങ്ങളുടെ ശേഖരം കാണുക
+
+Android- നായുള്ള J ദ്യോഗിക ജെല്ലിഫിൻ കമ്പാനിയൻ അപ്ലിക്കേഷനാണിത്. ജെല്ലിഫിൻ ഉപയോഗിച്ചതിന് നന്ദി!
diff --git a/fastlane/metadata/android/ml/short_description.txt b/fastlane/metadata/android/ml/short_description.txt
new file mode 100644
index 0000000..7f2be46
--- /dev/null
+++ b/fastlane/metadata/android/ml/short_description.txt
@@ -0,0 +1 @@
+സ software ജന്യ സോഫ്റ്റ്വെയർ മീഡിയ സിസ്റ്റമായ ജെല്ലിഫിനായുള്ള മൊബൈൽ ക്ലയന്റ്
diff --git a/fastlane/metadata/android/ml/title.txt b/fastlane/metadata/android/ml/title.txt
new file mode 100644
index 0000000..15117d8
--- /dev/null
+++ b/fastlane/metadata/android/ml/title.txt
@@ -0,0 +1 @@
+ജെല്ലിഫിൻ - നിങ്ങളുടെ മീഡിയ നിങ്ങളുടെ കൈയ്യിൽ!
diff --git a/fastlane/metadata/android/nb_NO/changelogs/default.txt b/fastlane/metadata/android/nb_NO/changelogs/default.txt
new file mode 100644
index 0000000..da1473d
--- /dev/null
+++ b/fastlane/metadata/android/nb_NO/changelogs/default.txt
@@ -0,0 +1,3 @@
+Takk for at du bruker jellyfin for Android! Det er alltid anbefalt å bruke den nyeste versjonen av Jellyfin-serveren,
+
+For mer informasjon kan du se på vår blogg på jellyfin.org, eller lese endringsloggen på GitHub.
diff --git a/fastlane/metadata/android/nb_NO/full_description.txt b/fastlane/metadata/android/nb_NO/full_description.txt
new file mode 100644
index 0000000..581d53f
--- /dev/null
+++ b/fastlane/metadata/android/nb_NO/full_description.txt
@@ -0,0 +1,14 @@
+Dine mediefiler, på dine vilkår.
+
+Jellyfin-prosjektet er en åpen-kildekode, gratis medieserver. Ingen avgifter, ingen sporing, ingen skjult agenda. Last ned vår gratis server for å samle all lyd, video, bilder og mer på ett sted.
+
+For å bruke appen må du ha en Jellyfin-server satt opp. Finn ut mer på jellyfin.org.
+
+Med en Jellyfin-server kan du:
+
+* Se live-TV og innspilte show fra Jellyfin-serveren din (ekstra maskinvare/tjenester kreves)
+* Strøm til en Chromecast-enhet på nettverket ditt
+* Strøm media til Android-enheten din
+* Se samlingen din i et brukervennlig grensesnitt
+
+Dette er den offisielle Jellyfin-appen for Android. Takk for at du bruker Jellyfin!
diff --git a/fastlane/metadata/android/nb_NO/short_description.txt b/fastlane/metadata/android/nb_NO/short_description.txt
new file mode 100644
index 0000000..bb2c212
--- /dev/null
+++ b/fastlane/metadata/android/nb_NO/short_description.txt
@@ -0,0 +1 @@
+Mobilklient for Jellyfin, et gratis multimediasenter
diff --git a/fastlane/metadata/android/nb_NO/title.txt b/fastlane/metadata/android/nb_NO/title.txt
new file mode 100644
index 0000000..d80462a
--- /dev/null
+++ b/fastlane/metadata/android/nb_NO/title.txt
@@ -0,0 +1 @@
+Jellyfin - dine medier i dine hender!
diff --git a/fastlane/metadata/android/nl/changelogs/default.txt b/fastlane/metadata/android/nl/changelogs/default.txt
new file mode 100644
index 0000000..b2d2478
--- /dev/null
+++ b/fastlane/metadata/android/nl/changelogs/default.txt
@@ -0,0 +1,3 @@
+Bedankt voor het gebruiken van Jellyfin voor Android! Het wordt aangeraden om altijd de nieuwste versie van de Jellyfin Server te gebruiken.
+
+Voor meer informatie, bekijk onze blog op jellyfin.org of lees de volledige lijst met veranderingen op Github.
diff --git a/fastlane/metadata/android/nl/full_description.txt b/fastlane/metadata/android/nl/full_description.txt
new file mode 100644
index 0000000..32e156e
--- /dev/null
+++ b/fastlane/metadata/android/nl/full_description.txt
@@ -0,0 +1,14 @@
+Jouw media, op jouw voorwaarden.
+
+Het Jellyfin-project is een open source, gratis software mediaserver. Geen kosten, geen tracking, geen verborgen agenda. Gebruik onze gratis server om al je audio, video, foto's en meer op één plek te verzamelen.
+
+Om de app te gebruiken, moet je een Jellyfin-server hebben opgezet en draaiend hebben. Meer informatie vind je op jellyfin.org.
+
+Met een Jellyfin-server kun je:
+
+* Live-tv kijken en opgenomen shows bekijken vanaf jouw Jellyfin-server (extra hardware/diensten vereist)
+* Streamen naar een Chromecast-apparaat op jouw netwerk
+* Je media streamen naar je Android-apparaat
+* Je collectie bekijken in een eenvoudig te gebruiken interface
+
+Dit is de officiële Jellyfin-app voor Android. Bedankt voor het gebruiken van Jellyfin!
diff --git a/fastlane/metadata/android/nl/short_description.txt b/fastlane/metadata/android/nl/short_description.txt
new file mode 100644
index 0000000..920f722
--- /dev/null
+++ b/fastlane/metadata/android/nl/short_description.txt
@@ -0,0 +1 @@
+Mobiele app voor Jellyfin, het vrije mediasysteem
diff --git a/fastlane/metadata/android/nl/title.txt b/fastlane/metadata/android/nl/title.txt
new file mode 100644
index 0000000..3043714
--- /dev/null
+++ b/fastlane/metadata/android/nl/title.txt
@@ -0,0 +1 @@
+Jellyfin - Jouw media in eigen handen!
diff --git a/fastlane/metadata/android/pl/changelogs/default.txt b/fastlane/metadata/android/pl/changelogs/default.txt
new file mode 100644
index 0000000..a3d6206
--- /dev/null
+++ b/fastlane/metadata/android/pl/changelogs/default.txt
@@ -0,0 +1,3 @@
+Dziękujemy za używanie Jellyfin na Android TV! Rekomendujemy używanie najnowszej wersji serwera Jellyfin.
+
+Po więcej informacji sprawdź naszego bloga na jellyfin.org lub przeczytaj całą listę zmian na GitHubie.
diff --git a/fastlane/metadata/android/pl/full_description.txt b/fastlane/metadata/android/pl/full_description.txt
new file mode 100644
index 0000000..8325e42
--- /dev/null
+++ b/fastlane/metadata/android/pl/full_description.txt
@@ -0,0 +1,14 @@
+Twoje media, na twoich warunkach.
+
+Projekt Jellyfin jest open source, wolnym oprogramowaniem serwera mediów. Bez opłat, bez śledzenia, bez ukrytych planów. Pobierz nasz darmowy serwer, aby zebrać wszystkie swoje pliki audio, wideo, zdjęcia i inne w jednym miejscu.
+
+Aby korzystać z aplikacji, musisz mieć skonfigurowany i uruchomiony serwer Jellyfin. Dowiedz się więcej na jellyfin.org.
+
+Z serwerem Jellyfin, możesz:
+
+* Oglądać telewizję na żywo i nagrane programy z Twojego serwera Jellyfin (wymagany dodatkowy sprzęt/usługi)
+* Strumieniować do urządzenia Chromecast w twojej sieci
+* Strumieniować media do urządzenia z systemem Android
+* Przeglądaj swoją kolekcję w łatwym w użyciu interfejsie.
+
+To jest oficjalna aplikacja towarzysząca Jellyfin dla Androida. Dziękujemy za korzystanie z Jellyfin!
diff --git a/fastlane/metadata/android/pl/short_description.txt b/fastlane/metadata/android/pl/short_description.txt
new file mode 100644
index 0000000..1ea682a
--- /dev/null
+++ b/fastlane/metadata/android/pl/short_description.txt
@@ -0,0 +1 @@
+Mobilny klient dla Jellyfin, darmowy system multimedialny
diff --git a/fastlane/metadata/android/pl/title.txt b/fastlane/metadata/android/pl/title.txt
new file mode 100644
index 0000000..01c06a7
--- /dev/null
+++ b/fastlane/metadata/android/pl/title.txt
@@ -0,0 +1 @@
+Jellyfin - Twoje media w Twoich rękach!
diff --git a/fastlane/metadata/android/pt-BR/changelogs/default.txt b/fastlane/metadata/android/pt-BR/changelogs/default.txt
new file mode 100644
index 0000000..78f5b9e
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/changelogs/default.txt
@@ -0,0 +1,3 @@
+Obrigado por usar o Jellyfin para Android! Usar o servidor Jellyfin mais recente é sempre recomendado.
+
+Para obter mais informações, consulte nosso blog em jellyfin.org ou leia o todas as mudanças no GitHub.
diff --git a/fastlane/metadata/android/pt-BR/full_description.txt b/fastlane/metadata/android/pt-BR/full_description.txt
new file mode 100644
index 0000000..e0e1b0a
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/full_description.txt
@@ -0,0 +1,14 @@
+Sua mídia, suas regras.
+
+O projeto Jellyfin é um sistema de mídia de software livre, código aberto. Sem custos, sem telemetria e sem motivações duvidosas. Obtenha o servidor gratuitamente para salvar todos os seus áudios, vídeos, fotos e muito mais em um só lugar.
+
+Para usar o aplicativo, você precisa ter um servidor Jellyfin configurado e rodando. Saiba mais em jellyfin.org.
+
+Com um servidor Jellyfin, você poderá:
+
+* Assistir TV ao vivo e programas gravados no seu servidor Jellyfin (hardware e serviços adicionais necessários)
+* Transmitir para um dispositivo Chromecast na sua rede
+* Transmitir sua mídia para o seu dispositivo Android
+* Ver sua coleção em uma interface fácil de usar
+
+Este é o aplicativo oficial do Jellyfin para Android. Obrigado por usar o Jellyfin!
diff --git a/fastlane/metadata/android/pt-BR/short_description.txt b/fastlane/metadata/android/pt-BR/short_description.txt
new file mode 100644
index 0000000..bd2cf8f
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/short_description.txt
@@ -0,0 +1 @@
+Cliente móvel para o Jellyfin, o sistema de mídia de software livre
diff --git a/fastlane/metadata/android/pt-BR/title.txt b/fastlane/metadata/android/pt-BR/title.txt
new file mode 100644
index 0000000..62a90b7
--- /dev/null
+++ b/fastlane/metadata/android/pt-BR/title.txt
@@ -0,0 +1 @@
+Jellyfin - sua mídia em suas mãos!
diff --git a/fastlane/metadata/android/pt-PT/changelogs/default.txt b/fastlane/metadata/android/pt-PT/changelogs/default.txt
new file mode 100644
index 0000000..14aba1e
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/changelogs/default.txt
@@ -0,0 +1,3 @@
+Obrigado por usares o Jellyfin para Android! Recomenda-se sempre o uso da versão mais recente do servidor Jellyfin.
+
+Para mais informações, consulta o nosso blogue em jellyfin.org, ou lê o registo de alterações completo no GitHub.
diff --git a/fastlane/metadata/android/pt-PT/full_description.txt b/fastlane/metadata/android/pt-PT/full_description.txt
new file mode 100644
index 0000000..4c386d6
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/full_description.txt
@@ -0,0 +1,14 @@
+O teu conteúdo multimédia, da forma que desejares.
+
+O projeto Jellyfin é um servidor multimédia gratuito e de código aberto. Sem taxas, sem rastreamento e sem interesses ocultos. Obtém o nosso servidor gratuito para reunir todo o teu áudio, vídeo, fotografias e muito mais num único local.
+
+Para utilizar a aplicação, tens de ter um servidor Jellyfin configurado e a funcionar. Descobre mais em jellyfin.org.
+
+Com um servidor Jellyfin, podes:
+
+* Ver TV em direto e programas gravados a partir do teu servidor Jellyfin (é necessário hardware/serviços adicionais)
+* Transmitir para um dispositivo Chromecast na tua rede
+* Transmitir os teus conteúdos multimédia para o teu dispositivo Android
+* Ver a tua coleção numa interface fácil de utilizar
+
+Esta é a aplicação oficial do Jellyfin para Android. Obrigado por usares o Jellyfin!
diff --git a/fastlane/metadata/android/pt-PT/short_description.txt b/fastlane/metadata/android/pt-PT/short_description.txt
new file mode 100644
index 0000000..0c09ee0
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/short_description.txt
@@ -0,0 +1 @@
+Cliente móvel para Jellyfin, o sistema multimédia de software livre
diff --git a/fastlane/metadata/android/pt-PT/title.txt b/fastlane/metadata/android/pt-PT/title.txt
new file mode 100644
index 0000000..b75f1be
--- /dev/null
+++ b/fastlane/metadata/android/pt-PT/title.txt
@@ -0,0 +1 @@
+Jellyfin - A tua multimédia nas tuas mãos!
diff --git a/fastlane/metadata/android/pt/changelogs/default.txt b/fastlane/metadata/android/pt/changelogs/default.txt
new file mode 100644
index 0000000..14aba1e
--- /dev/null
+++ b/fastlane/metadata/android/pt/changelogs/default.txt
@@ -0,0 +1,3 @@
+Obrigado por usares o Jellyfin para Android! Recomenda-se sempre o uso da versão mais recente do servidor Jellyfin.
+
+Para mais informações, consulta o nosso blogue em jellyfin.org, ou lê o registo de alterações completo no GitHub.
diff --git a/fastlane/metadata/android/pt/full_description.txt b/fastlane/metadata/android/pt/full_description.txt
new file mode 100644
index 0000000..4c386d6
--- /dev/null
+++ b/fastlane/metadata/android/pt/full_description.txt
@@ -0,0 +1,14 @@
+O teu conteúdo multimédia, da forma que desejares.
+
+O projeto Jellyfin é um servidor multimédia gratuito e de código aberto. Sem taxas, sem rastreamento e sem interesses ocultos. Obtém o nosso servidor gratuito para reunir todo o teu áudio, vídeo, fotografias e muito mais num único local.
+
+Para utilizar a aplicação, tens de ter um servidor Jellyfin configurado e a funcionar. Descobre mais em jellyfin.org.
+
+Com um servidor Jellyfin, podes:
+
+* Ver TV em direto e programas gravados a partir do teu servidor Jellyfin (é necessário hardware/serviços adicionais)
+* Transmitir para um dispositivo Chromecast na tua rede
+* Transmitir os teus conteúdos multimédia para o teu dispositivo Android
+* Ver a tua coleção numa interface fácil de utilizar
+
+Esta é a aplicação oficial do Jellyfin para Android. Obrigado por usares o Jellyfin!
diff --git a/fastlane/metadata/android/pt/short_description.txt b/fastlane/metadata/android/pt/short_description.txt
new file mode 100644
index 0000000..0c09ee0
--- /dev/null
+++ b/fastlane/metadata/android/pt/short_description.txt
@@ -0,0 +1 @@
+Cliente móvel para Jellyfin, o sistema multimédia de software livre
diff --git a/fastlane/metadata/android/pt/title.txt b/fastlane/metadata/android/pt/title.txt
new file mode 100644
index 0000000..b75f1be
--- /dev/null
+++ b/fastlane/metadata/android/pt/title.txt
@@ -0,0 +1 @@
+Jellyfin - A tua multimédia nas tuas mãos!
diff --git a/fastlane/metadata/android/ro/changelogs/default.txt b/fastlane/metadata/android/ro/changelogs/default.txt
new file mode 100644
index 0000000..295b5b8
--- /dev/null
+++ b/fastlane/metadata/android/ro/changelogs/default.txt
@@ -0,0 +1,3 @@
+Îți mulțumim pentru că folosești Jellyfin pentru Android! Folosirea celei main noi versiuni de Jellyfin Server este întotdeauna recomandată.
+
+Pentru mai multe informații, vă rugăm să vizitați blogul nostru pe jellyfin.org, sau să citiți întregul jurnal de modificări pe GitHub.
diff --git a/fastlane/metadata/android/ro/full_description.txt b/fastlane/metadata/android/ro/full_description.txt
new file mode 100644
index 0000000..1cb5bc8
--- /dev/null
+++ b/fastlane/metadata/android/ro/full_description.txt
@@ -0,0 +1,14 @@
+Conținutul tău media, regulile tale.
+
+Proiectul Jellyfin este un server de conținut media gratuit, cu sursă deschisă. Fără taxe, fără tracking, fără agendă ascunsă. Instalați serverul nostru gratuit pentru a aduna toate fișierele audio, video, pozele și altele într-un singur loc.
+
+Pentru a folosi această aplicație, trebuie să ai un server Jellyfin configurat și care rulează. Află mai multe pe jellyfin.org.
+
+Cu un server Jellyfin, poți să:
+
+* Urmărești TV în direct și emisiuni înregistrate de pe serverul tău Jellyfin (hardware/servicii suplimentare sunt necesare)
+* Transmiți în flux pe un dispozitiv Chromecast din rețeaua dvs
+* Transmiti conținut media pe dispozitivul tău Android
+* Vizualizezi colecția ta într-o interfață ușor de utilizat
+
+Aceasta este aplicația oficială Jellyfin pentru Android. Mulțumim că folosești Jellyfin!
diff --git a/fastlane/metadata/android/ro/short_description.txt b/fastlane/metadata/android/ro/short_description.txt
new file mode 100644
index 0000000..fa76056
--- /dev/null
+++ b/fastlane/metadata/android/ro/short_description.txt
@@ -0,0 +1 @@
+Aplicația pentru mobil Jellyfin, sistemul de conținut media gratuit
diff --git a/fastlane/metadata/android/ro/title.txt b/fastlane/metadata/android/ro/title.txt
new file mode 100644
index 0000000..fc8b4a8
--- /dev/null
+++ b/fastlane/metadata/android/ro/title.txt
@@ -0,0 +1 @@
+Jellyfin - conținutul tău în mâinile tale!
diff --git a/fastlane/metadata/android/ru/changelogs/default.txt b/fastlane/metadata/android/ru/changelogs/default.txt
new file mode 100644
index 0000000..99ced9b
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/default.txt
@@ -0,0 +1,3 @@
+Спасибо за использование Jellyfin для Android! Всегда рекомендуем использовать последнюю версию сервера Jellyfin.
+
+За более подробной информацией посетите наш блог на jellyfin.org, или прочтите историю изменений на GitHub.
diff --git a/fastlane/metadata/android/ru/full_description.txt b/fastlane/metadata/android/ru/full_description.txt
new file mode 100644
index 0000000..3fb4b84
--- /dev/null
+++ b/fastlane/metadata/android/ru/full_description.txt
@@ -0,0 +1,14 @@
+Ваши медиаданные на ваших условиях.
+
+Проект Jellyfin — это медиа-сервер на свободном программном обеспечении с открытым исходным кодом. Никакой платы, никакого слежения, никаких скрытых намерений. Обретите наш свободный сервер и соберите все ваши аудио, видео, фотографии и многое другое в одном месте.
+
+Чтобы использовать приложение, у вас должен быть настроен и запущен Jellyfin-сервер. Подробней на jellyfin.org .
+
+С Jellyfin-сервером вы сможете:
+
+* Смотреть ТВ-эфир и записи передач с вашего сервера (требуется дополнительное оборудование / службы);
+* Транслировать на Chromecast-устройство в вашей сети;
+* Транслировать свои медиаданные на Android-устройство;
+* Просматривать свою коллекцию в удобном интерфейсе.
+
+Это — официальное клиентское приложение Jellyfin для Android. Благодарим за использование Jellyfin!
diff --git a/fastlane/metadata/android/ru/short_description.txt b/fastlane/metadata/android/ru/short_description.txt
new file mode 100644
index 0000000..7e5d8ed
--- /dev/null
+++ b/fastlane/metadata/android/ru/short_description.txt
@@ -0,0 +1 @@
+Мобильный клиент для Jellyfin, свободной медиасистемы
diff --git a/fastlane/metadata/android/ru/title.txt b/fastlane/metadata/android/ru/title.txt
new file mode 100644
index 0000000..1472e0b
--- /dev/null
+++ b/fastlane/metadata/android/ru/title.txt
@@ -0,0 +1 @@
+Jellyfin — всё в ваших руках!
diff --git a/fastlane/metadata/android/sk/changelogs/default.txt b/fastlane/metadata/android/sk/changelogs/default.txt
new file mode 100644
index 0000000..c17f899
--- /dev/null
+++ b/fastlane/metadata/android/sk/changelogs/default.txt
@@ -0,0 +1,3 @@
+Ďakujeme, že používate Jellyfin pre Android! Vždy odporúčame používať najnovšiu verziu servera Jellyfin.
+
+Viac informácií nájdete na našom blogu jellyfin.org alebo si prečítajte celý zoznam zmien na GitHube.
diff --git a/fastlane/metadata/android/sk/full_description.txt b/fastlane/metadata/android/sk/full_description.txt
new file mode 100644
index 0000000..22c6d99
--- /dev/null
+++ b/fastlane/metadata/android/sk/full_description.txt
@@ -0,0 +1,14 @@
+Vaše médiá, vaše podmienky.
+
+Projekt Jellyfin je open source, mediálny software, ktorý je zdarma. Žiadne poplatky, žiadne sledovanie, žiadna skrytá agenda. Skúste náš softvér zdarma a užívajte si svoju kolekciu audia, videa, fotografií a oveľa viac na jednom mieste.
+
+Aby ste mohli použiť aplikáciu, potrebujete mať nastavený a spustený Jellyfin server. Zistite viac na jellyfin.org.
+
+S Jellyfin serverom môžte:
+
+* Sledovať a nahrávať živú TV z Jellyfin servera (vyžaduje dodatočný hardvér/služby)
+* Streamovať na Chromecast zariadenia na vašej sieti
+* Streamovať vaše médiá na vaše Android zariadenie
+* Prehliadať vašu kolekciu v rozhraní, ktoré sa ľahko používa
+
+Toto je oficiálna Jellyfin aplikácia pre Android. Ďakujeme, že používate Jellyfin!
diff --git a/fastlane/metadata/android/sk/short_description.txt b/fastlane/metadata/android/sk/short_description.txt
new file mode 100644
index 0000000..e9b8aab
--- /dev/null
+++ b/fastlane/metadata/android/sk/short_description.txt
@@ -0,0 +1 @@
+Mobilný klient pre Jellyfin, mediálny softvér, ktorý je úplne zdarma
diff --git a/fastlane/metadata/android/sk/title.txt b/fastlane/metadata/android/sk/title.txt
new file mode 100644
index 0000000..5660187
--- /dev/null
+++ b/fastlane/metadata/android/sk/title.txt
@@ -0,0 +1 @@
+Jellyfin - vaše médiá vo vašich rukách!
diff --git a/fastlane/metadata/android/sl/changelogs/default.txt b/fastlane/metadata/android/sl/changelogs/default.txt
new file mode 100644
index 0000000..9ec2bcb
--- /dev/null
+++ b/fastlane/metadata/android/sl/changelogs/default.txt
@@ -0,0 +1,3 @@
+Hvala vam da uporabljate Jellyfin za Android. Priporočamo uporabe zadnje verzije Jellyfin strežnika.
+
+Za več informacij si oglejte blog na jellyfin.org ali celoten seznam sprememb na GitHub.
diff --git a/fastlane/metadata/android/sl/full_description.txt b/fastlane/metadata/android/sl/full_description.txt
new file mode 100644
index 0000000..f4ccf5b
--- /dev/null
+++ b/fastlane/metadata/android/sl/full_description.txt
@@ -0,0 +1,14 @@
+Vaša predstavnost, vaša pravila.
+
+Jellyfin je odprtokodni, prosti predstavnostni strežnik. Brez stroškov, brez sledenja in brez skritih namenov. Pridobite naš brezplačni strežnik in začnite zbirati vso vašo glasbo, videoposnetke in slike na enem mestu.
+
+Za uporabo aplikacije potrebujete delujoč Jellyfin strežnik. Več informacij na jellyfin.org.
+
+Z Jellyfin strežnikom lahko:
+
+* Gledate TV v živo in posnetke preteklih oddaj z vašega Jellyfin strežnika (zahteva dodatno strojno opremo/storitve)
+* Pretakate vsebine na naprave Chromecast v vašem omrežju
+* Pretakate predstavnost na vašo napravo Android
+* Si ogledate vašo zbirko z enostavnem vmesnikom.
+
+To je uradni odjemalec Jellyfin za Android. Hvala, ker uporabljate Jellyfin!
diff --git a/fastlane/metadata/android/sl/short_description.txt b/fastlane/metadata/android/sl/short_description.txt
new file mode 100644
index 0000000..5a10791
--- /dev/null
+++ b/fastlane/metadata/android/sl/short_description.txt
@@ -0,0 +1 @@
+Mobilni odjemalec za odprtokodni predstavnosti strežnik Jellyfin
diff --git a/fastlane/metadata/android/sl/title.txt b/fastlane/metadata/android/sl/title.txt
new file mode 100644
index 0000000..b344ae3
--- /dev/null
+++ b/fastlane/metadata/android/sl/title.txt
@@ -0,0 +1 @@
+Jellyfin - vaša predstavnost v vaših rokah!
diff --git a/fastlane/metadata/android/sr/full_description.txt b/fastlane/metadata/android/sr/full_description.txt
new file mode 100644
index 0000000..4251d13
--- /dev/null
+++ b/fastlane/metadata/android/sr/full_description.txt
@@ -0,0 +1,14 @@
+Ваши медији, под вашим условима.
+
+Пројекат Jellyfin је бесплатни медијски сервер отвореног кода. Без накнаде, без праћења, без скривене агенде. Набавите наш бесплатни сервер за прикупљање свих ваших аудио, видео записа, фотографија и још много тога на једном месту.
+
+Да бисте користили апликацију, морате имати постављен и покренут Jellyfin сервер. Сазнајте више на jellyfin.org.
+
+Помоћу Jellyfin сервера можете да:
+
+* Гледајте ТВ уживо и снимљене емисије са вашег Jellyfin сервера (потребан је додатни хардвер / услуге)
+* Стримујте на Chromecast уређај на својој мрежи
+* Стримујте своје медије на Андроид уређај
+* Прегледате своју колекцију у интерфејсу једноставном за употребу
+
+Ово је званична пратећа апликација Jellyfin за Андроид. Хвала вам што користите Jellyfin!
diff --git a/fastlane/metadata/android/sr/short_description.txt b/fastlane/metadata/android/sr/short_description.txt
new file mode 100644
index 0000000..3b7baf6
--- /dev/null
+++ b/fastlane/metadata/android/sr/short_description.txt
@@ -0,0 +1 @@
+Мобилни клијент за Jellyfin, бесплатан медијски систем
diff --git a/fastlane/metadata/android/sr/title.txt b/fastlane/metadata/android/sr/title.txt
new file mode 100644
index 0000000..478dcb3
--- /dev/null
+++ b/fastlane/metadata/android/sr/title.txt
@@ -0,0 +1 @@
+Jellyfin - ваши медији у вашим рукама!
diff --git a/fastlane/metadata/android/sv/changelogs/default.txt b/fastlane/metadata/android/sv/changelogs/default.txt
new file mode 100644
index 0000000..b72caa4
--- /dev/null
+++ b/fastlane/metadata/android/sv/changelogs/default.txt
@@ -0,0 +1,3 @@
+Tack för att du använder Jellyfin för Android! Det är alltid rekommenderat att använda den senast släppta Jellyfin Server.
+
+För mer information, vänligen se vår blogg på jellyfin.org, eller läs hela ändringsloggen på GitHub.
diff --git a/fastlane/metadata/android/sv/full_description.txt b/fastlane/metadata/android/sv/full_description.txt
new file mode 100644
index 0000000..06ddde0
--- /dev/null
+++ b/fastlane/metadata/android/sv/full_description.txt
@@ -0,0 +1,14 @@
+Din media, på dina villkor.
+
+Jellyfin är ett kostnadsfritt mediaserverprojekt med öppen källkod. Inga avgifter, ingen spårning och inga dolda motiv. Ta del av detta projekt och ladda ner din gratisserver för att samla dina videor, ljudspår, foton och mer på ett och samma ställe.
+
+För att kunna använda appen krävs det att du har konfigurerat och kör en server. Få reda på mer hos jellyfin.org
+
+Med en Jellyfin-server kan du:
+
+*Kolla på direktsänd TV och spela dina sparade shower från din Jellyfin-server (ytterligare hårdvara/tjänster krävs)
+*Strömma till en Chromecast-enhet på ditt nätverk.
+*Strömma din media till din Android-enhet.
+*Ta del av din samling genom ett användarvänligt gränssnitt.
+
+Detta är den officiella Jellyfin-appen för Android. Tack för att du har valt Jellyfin!
diff --git a/fastlane/metadata/android/sv/short_description.txt b/fastlane/metadata/android/sv/short_description.txt
new file mode 100644
index 0000000..f6451d9
--- /dev/null
+++ b/fastlane/metadata/android/sv/short_description.txt
@@ -0,0 +1 @@
+Mobilklient för Jellyfin, den kostnadsfria mjukvaran för mediasystem
diff --git a/fastlane/metadata/android/sv/title.txt b/fastlane/metadata/android/sv/title.txt
new file mode 100644
index 0000000..a4852c9
--- /dev/null
+++ b/fastlane/metadata/android/sv/title.txt
@@ -0,0 +1 @@
+Jellyfin - Din media i dina händer!
diff --git a/fastlane/metadata/android/sw/full_description.txt b/fastlane/metadata/android/sw/full_description.txt
new file mode 100644
index 0000000..aadcc5c
--- /dev/null
+++ b/fastlane/metadata/android/sw/full_description.txt
@@ -0,0 +1,14 @@
+Vyombo vya habari vyako, kwa masharti yako.
+
+Mradi wa Jellyfin ni chanzo wazi, seva ya media ya programu huria. Hakuna ada, hakuna ufuatiliaji, hakuna ajenda iliyofichwa. Pata seva yetu isiyolipishwa ili kukusanya sauti, video, picha zako zote na zaidi katika sehemu moja.
+
+Ili kutumia programu, lazima uwe na seva ya Jellyfin iliyosanidiwa na kufanya kazi. Pata maelezo zaidi katika jellyfin.org.
+
+Ukiwa na seva ya Jellyfin, unaweza:
+
+Tazama TV ya Moja kwa Moja na vipindi vilivyorekodiwa kutoka kwa seva yako ya Jellyfin (vifaa/huduma za ziada zinahitajika)
+Tiririsha kwenye kifaa cha Chromecast kwenye mtandao wako
+Tiririsha midia yako kwenye kifaa chako cha Android
+Tazama mkusanyiko wako katika kiolesura rahisi kutumia
+
+Hii ndio programu rasmi ya Jellyfin ya Android. Asante kwa kutumia!
diff --git a/fastlane/metadata/android/sw/short_description.txt b/fastlane/metadata/android/sw/short_description.txt
new file mode 100644
index 0000000..0317a82
--- /dev/null
+++ b/fastlane/metadata/android/sw/short_description.txt
@@ -0,0 +1 @@
+Mteja wa rununu wa Jellyfin, mfumo wa bure wa midia ya programu
diff --git a/fastlane/metadata/android/sw/title.txt b/fastlane/metadata/android/sw/title.txt
new file mode 100644
index 0000000..102314b
--- /dev/null
+++ b/fastlane/metadata/android/sw/title.txt
@@ -0,0 +1 @@
+burudani yako mikononi mwako!
diff --git a/fastlane/metadata/android/ta/changelogs/default.txt b/fastlane/metadata/android/ta/changelogs/default.txt
new file mode 100644
index 0000000..820f843
--- /dev/null
+++ b/fastlane/metadata/android/ta/changelogs/default.txt
@@ -0,0 +1,3 @@
+ஆண்ட்ராய்டுக்கு ஜெல்லிஃபினைப் பயன்படுத்தியதற்கு நன்றி! சமீபத்திய வெளியிடப்பட்ட ஜெல்லிஃபின் சேவையகத்தைப் பயன்படுத்துவது எப்போதும் பரிந்துரைக்கப்படுகிறது.
+
+மேலும் தகவலுக்கு, jellyfin.org இல் உள்ள எங்கள் வலைப்பதிவைப் பார்க்கவும் அல்லது GitHub இல் முழு சேஞ்ச்லாக்கைப் படிக்கவும்.
diff --git a/fastlane/metadata/android/ta/full_description.txt b/fastlane/metadata/android/ta/full_description.txt
new file mode 100644
index 0000000..84902a5
--- /dev/null
+++ b/fastlane/metadata/android/ta/full_description.txt
@@ -0,0 +1,14 @@
+உங்கள் மீடியா, உங்கள் விதிமுறைகளின்படி.
+
+ஜெல்லிஃபின் திட்டம் ஒரு திறந்த மூல, இலவச மென்பொருள் ஊடக சேவையகம். கட்டணம் இல்லை, கண்காணிப்பு இல்லை, மறைக்கப்பட்ட நிகழ்ச்சி நிரல் இல்லை. உங்கள் ஆடியோ, வீடியோ, புகைப்படங்கள் மற்றும் பலவற்றை ஒரே இடத்தில் சேகரிக்க எங்கள் இலவச சேவையகத்தைப் பெறுங்கள்.
+
+பயன்பாட்டைப் பயன்படுத்த, நீங்கள் ஜெல்லிஃபின் சேவையகத்தை அமைத்து இயக்க வேண்டும். jellyfin.org இல் மேலும் அறியவும்.
+
+ஜெல்லிஃபின் சர்வர் மூலம், உங்களால் முடியும்:
+
+* உங்கள் ஜெல்லிஃபின் சேவையகத்திலிருந்து நேரலை டிவி மற்றும் பதிவுசெய்யப்பட்ட நிகழ்ச்சிகளைப் பார்க்கவும் (கூடுதல் வன்பொருள்/சேவைகள் தேவை)
+* உங்கள் நெட்வொர்க்கில் உள்ள Chromecast சாதனத்திற்கு ஸ்ட்ரீம் செய்யவும்
+* உங்கள் மீடியாவை உங்கள் Android சாதனத்தில் ஸ்ட்ரீம் செய்யவும்
+* பயன்படுத்த எளிதான இடைமுகத்தில் உங்கள் சேகரிப்பைப் பார்க்கவும்
+
+இது ஆண்ட்ராய்டுக்கான அதிகாரப்பூர்வ ஜெல்லிஃபின் துணைப் பயன்பாடாகும். ஜெல்லிஃபினைப் பயன்படுத்தியதற்கு நன்றி!
diff --git a/fastlane/metadata/android/ta/short_description.txt b/fastlane/metadata/android/ta/short_description.txt
new file mode 100644
index 0000000..76cbc05
--- /dev/null
+++ b/fastlane/metadata/android/ta/short_description.txt
@@ -0,0 +1 @@
+இலவச மென்பொருள் ஊடக அமைப்பான ஜெல்லிஃபினுக்கான மொபைல் கிளையன்ட்
diff --git a/fastlane/metadata/android/ta/title.txt b/fastlane/metadata/android/ta/title.txt
new file mode 100644
index 0000000..f2c03a9
--- /dev/null
+++ b/fastlane/metadata/android/ta/title.txt
@@ -0,0 +1 @@
+ஜெல்லிஃபின் - உங்கள் ஊடகம்!
diff --git a/fastlane/metadata/android/te/full_description.txt b/fastlane/metadata/android/te/full_description.txt
new file mode 100644
index 0000000..5b62f56
--- /dev/null
+++ b/fastlane/metadata/android/te/full_description.txt
@@ -0,0 +1,14 @@
+మీ మీడియా, మీ నిబంధనల ప్రకారం.
+
+జెJellyfin ప్రాజెక్ట్ ఒక ఓపెన్ సోర్స్, ఉచిత సాఫ్ట్వేర్ మీడియా సర్వర్. ఫీజులు లేవు, ట్రాకింగ్ లేదు, దాచిన ఎజెండా లేదు. మీ ఆడియో, వీడియో, ఫోటోలు మరియు మరిన్నింటిని ఒకే చోట సేకరించడానికి మా ఉచిత సర్వర్ని పొందండి.
+
+యాప్ని ఉపయోగించడానికి, మీరు తప్పనిసరిగా జెల్లీఫిన్ సర్వర్ని సెటప్ చేసి, రన్ చేసి ఉండాలి. వద్ద మరింత తెలుసుకోండి jellyfin.org.
+
+Jellyfin సర్వర్తో, మీరు వీటిని చేయవచ్చు:
+
+* మీ Jellyfin సర్వర్ నుండి ప్రత్యక్ష టీవీ మరియు రికార్డ్ చేసిన షోలను చూడండి (అదనపు హార్డ్వేర్/సర్వీసులు అవసరం)
+* మీ నెట్వర్క్లోని Chromecast పరికరానికి ప్రసారం చేయండి
+* మీ మీడియాను మీ Android పరికరానికి ప్రసారం చేయండి
+* మీ సేకరణను ఉపయోగించడానికి సులభమైన ఇంటర్ఫేస్లో వీక్షించండి
+
+ఇది Android కోసం అధికారిక Jellyfin కంపానియన్ యాప్. Jellyfin ఉపయోగించినందుకు ధన్యవాదాలు!
diff --git a/fastlane/metadata/android/te/short_description.txt b/fastlane/metadata/android/te/short_description.txt
new file mode 100644
index 0000000..7e72521
--- /dev/null
+++ b/fastlane/metadata/android/te/short_description.txt
@@ -0,0 +1 @@
+Jellyfin కోసం మొబైల్ క్లయింట్, ఉచిత సాఫ్ట్వేర్ మీడియా సిస్టమ్
diff --git a/fastlane/metadata/android/te/title.txt b/fastlane/metadata/android/te/title.txt
new file mode 100644
index 0000000..f9b770a
--- /dev/null
+++ b/fastlane/metadata/android/te/title.txt
@@ -0,0 +1 @@
+Jellyfin - మీ చేతుల్లో మీ మీడియా!
diff --git a/fastlane/metadata/android/tr/changelogs/default.txt b/fastlane/metadata/android/tr/changelogs/default.txt
new file mode 100644
index 0000000..3245b82
--- /dev/null
+++ b/fastlane/metadata/android/tr/changelogs/default.txt
@@ -0,0 +1,3 @@
+Android için olan Jellyfin uygulamasını kullandığınız için teşekkür ederiz! En son yayınlanan Jellyfin Sunucusunun kullanılması her zaman tavsiye edilir.
+
+Daha fazla bilgi için lütfen jellyfin.org adresindeki blogumuza bakın veya GitHub'daki değişiklik günlüğünün tamamını okuyun.
diff --git a/fastlane/metadata/android/tr/full_description.txt b/fastlane/metadata/android/tr/full_description.txt
new file mode 100644
index 0000000..66d59fc
--- /dev/null
+++ b/fastlane/metadata/android/tr/full_description.txt
@@ -0,0 +1,14 @@
+Sizin medyanız, sizin şartlarınıza göre.
+
+Jellyfin projesi açık kaynaklı, ücretsiz bir yazılım medya sunucusudur. Ücret yok, takip yok, gizli gündem yok. Tüm ses, video, fotoğraf ve daha fazlasını tek bir yerde toplamak için ücretsiz sunucumuzu edinin.
+
+Uygulamayı kullanmak için, kurulmuş ve çalışır durumda bir Jellyfin sunucunuz olmalıdır. jellyfin.org adresinde daha fazla bilgi edinin.
+
+Jellyfin sunucusuyla şunları yapabilirsiniz:
+
+* Jellyfin sunucunuzdan Canlı TV ve kaydedilmiş programları izleyin (ek donanım/hizmetler gereklidir)
+* Ağınızdaki bir Chromecast cihazına akış yapın
+* Medyanızı Android cihazınıza aktarın
+* Koleksiyonunuzu kullanımı kolay bir arayüzde görüntüleyin
+
+Bu, Android için resmi Jellyfin arkadaşı uygulamasıdır. Jellyfin'i kullandığınız için teşekkür ederiz!
diff --git a/fastlane/metadata/android/tr/short_description.txt b/fastlane/metadata/android/tr/short_description.txt
new file mode 100644
index 0000000..5a55fe7
--- /dev/null
+++ b/fastlane/metadata/android/tr/short_description.txt
@@ -0,0 +1 @@
+Özgür yazılım medya sistemi Jellyfin için mobil istemci
diff --git a/fastlane/metadata/android/tr/title.txt b/fastlane/metadata/android/tr/title.txt
new file mode 100644
index 0000000..411192a
--- /dev/null
+++ b/fastlane/metadata/android/tr/title.txt
@@ -0,0 +1 @@
+Jellyfin - Medyanız sizin elinizde!
diff --git a/fastlane/metadata/android/uk/changelogs/default.txt b/fastlane/metadata/android/uk/changelogs/default.txt
new file mode 100644
index 0000000..0345c59
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/default.txt
@@ -0,0 +1,3 @@
+Дякуємо за використання Jellyfin для Android! Ми завжди рекомендуємо використовувати останню версію сервера Jellyfin.
+
+Для отримання додаткової інформації, будь ласка, відвідайте наш блог на jellyfin.org або прочитайте повний список змін на GitHub.
diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt
new file mode 100644
index 0000000..a3b830c
--- /dev/null
+++ b/fastlane/metadata/android/uk/full_description.txt
@@ -0,0 +1,14 @@
+Ваші медіа - на ваших умовах.
+
+Проект Jellyfin - це безкоштовний медіасервер із відкритим вихідним кодом. Ні зборів, ні відстеження, ні прихованих агентів. Отримайте наш безкоштовний сервер, щоб зібрати всі ваші аудіо, відео, фотографії та інше в одному місці.
+
+Щоб користуватись додатком, у вас має бути налаштований і запущений сервер Jellyfin. Докладніше на jellyfin.org.
+
+За допомогою сервера Jellyfin ви можете:
+
+* Дивіться телебачення наживо та записані шоу з вашого сервера Jellyfin (потрібне додаткове обладнання/послуги)
+* Потокове передавання на пристрій Chromecast у вашій мережі
+* Перегляд медіафайлів за допомогою застосунку на пристрої Android
+* Перегляньте свою колекцію в простому у використанні інтерфейсі
+
+Це офіційний додаток - супутник Jellyfin для Android. Дякуємо, що використовуєте Jellyfin!
diff --git a/fastlane/metadata/android/uk/short_description.txt b/fastlane/metadata/android/uk/short_description.txt
new file mode 100644
index 0000000..b31f526
--- /dev/null
+++ b/fastlane/metadata/android/uk/short_description.txt
@@ -0,0 +1 @@
+Мобільний застосунок для Jellyfin - безкоштовної медіасистеми
diff --git a/fastlane/metadata/android/uk/title.txt b/fastlane/metadata/android/uk/title.txt
new file mode 100644
index 0000000..b8bf074
--- /dev/null
+++ b/fastlane/metadata/android/uk/title.txt
@@ -0,0 +1 @@
+Jellyfin - ваші медіа у ваших руках!
diff --git a/fastlane/metadata/android/ur/changelogs/default.txt b/fastlane/metadata/android/ur/changelogs/default.txt
new file mode 100644
index 0000000..134ba5e
--- /dev/null
+++ b/fastlane/metadata/android/ur/changelogs/default.txt
@@ -0,0 +1,3 @@
+Android کے لیے Jellyfin استعمال کرنے کا شکریہ! تازہ ترین جاری کردہ جیلیفن سرور استعمال کرنے کی ہمیشہ سفارش کی جاتی ہے۔
+
+مزید معلومات کے لیے، براہ کرم jellyfin.org پر ہمارا بلاگ دیکھیں، یا GitHub پر مکمل چینج لاگ پڑھیں۔
diff --git a/fastlane/metadata/android/ur/full_description.txt b/fastlane/metadata/android/ur/full_description.txt
new file mode 100644
index 0000000..e443c03
--- /dev/null
+++ b/fastlane/metadata/android/ur/full_description.txt
@@ -0,0 +1,14 @@
+آپ کا میڈیا ، آپ کی شرائط پر۔
+
+جیلی فن پروجیکٹ ایک اوپن سورس ، مفت سافٹ وئیر میڈیا سرور ہے۔ کوئی فیس ، کوئی ٹریکنگ ، کوئی پوشیدہ ایجنڈا نہیں۔ اپنے تمام آڈیو ، ویڈیو ، تصاویر اور بہت کچھ ایک جگہ جمع کرنے کے لیے ہمارا مفت سرور حاصل کریں۔
+
+ایپ کو استعمال کرنے کے لیے ، آپ کے پاس جیلی فن سرور کا سیٹ اپ اور اس کا چلنا ضروری ہے۔ jellyfin.org پر مزید معلومات حاصل کریں۔
+
+جیلی فن سرور کے ساتھ ، آپ یہ کر سکتے ہیں:
+
+* اپنے جیلی فین سرور سے براہ راست ٹی وی اور ریکارڈ شدہ شو دیکھیں (اضافی ہارڈ ویئر/خدمات درکار ہیں)
+* اپنے نیٹ ورک پر کروم کاسٹ ڈیوائس پر اسٹریم کریں۔
+* اپنے میڈیا کو اپنے اینڈرائڈ ڈیوائس پر اسٹریم کریں۔
+* استعمال میں آسان انٹرفیس میں اپنا کلیکشن دیکھیں۔
+
+یہ اینڈرائیڈ کے لیے آفیشل جیلی فین ساتھی ایپ ہے۔ جیلی فین استعمال کرنے کے لیے آپ کا شکریہ!
diff --git a/fastlane/metadata/android/ur/short_description.txt b/fastlane/metadata/android/ur/short_description.txt
new file mode 100644
index 0000000..403ae4d
--- /dev/null
+++ b/fastlane/metadata/android/ur/short_description.txt
@@ -0,0 +1 @@
+جیلی فن کے لئے موبائل کلائنٹ، مفت سافٹ وئیر میڈیا سسٹم
diff --git a/fastlane/metadata/android/ur/title.txt b/fastlane/metadata/android/ur/title.txt
new file mode 100644
index 0000000..938e38d
--- /dev/null
+++ b/fastlane/metadata/android/ur/title.txt
@@ -0,0 +1 @@
+- آپ کا میڈیا آپ کے ہاتھ میں!
diff --git a/fastlane/metadata/android/vi/changelogs/default.txt b/fastlane/metadata/android/vi/changelogs/default.txt
new file mode 100644
index 0000000..9837889
--- /dev/null
+++ b/fastlane/metadata/android/vi/changelogs/default.txt
@@ -0,0 +1,3 @@
+Cảm ơn bạn đã sử dụng Jellyfin cho Android! Luôn luôn khuyến khích sử dụng Máy chủ Jellyfin được phát hành mới nhất.
+
+Để biết thêm thông tin, vui lòng xem blog của chúng tôi tại jellyfin.org hoặc đọc nhật ký thay đổi đầy đủ trên GitHub.
diff --git a/fastlane/metadata/android/vi/full_description.txt b/fastlane/metadata/android/vi/full_description.txt
new file mode 100644
index 0000000..5a6fdfa
--- /dev/null
+++ b/fastlane/metadata/android/vi/full_description.txt
@@ -0,0 +1,14 @@
+Phương tiện của bạn, theo điều kiện của bạn.
+
+Dự án Jellyfin là một máy chủ đa phương tiện phần mềm miễn phí mã nguồn mở. Không có phí, không theo dõi, không có chương trình làm việc ẩn. Nhận máy chủ miễn phí của chúng tôi để thu thập tất cả âm thanh, video, ảnh và hơn thế nữa của bạn vào một nơi.
+
+Để sử dụng ứng dụng, bạn phải thiết lập và chạy máy chủ Jellyfin. Tìm hiểu thêm tại jellyfin.org.
+
+Với máy chủ Jellyfin, bạn có thể:
+
+* Xem Truyền hình Trực tiếp và các chương trình đã ghi từ máy chủ Jellyfin của bạn (yêu cầu phần cứng / dịch vụ bổ sung)
+* Truyền trực tuyến tới thiết bị Chromecast trên mạng của bạn
+* Truyền phương tiện của bạn đến thiết bị Android của bạn
+* Xem bộ sưu tập của bạn trong một giao diện dễ sử dụng
+
+Đây là ứng dụng đồng hành Jellyfin chính thức dành cho Android. Cảm ơn bạn đã sử dụng Jellyfin!
diff --git a/fastlane/metadata/android/vi/short_description.txt b/fastlane/metadata/android/vi/short_description.txt
new file mode 100644
index 0000000..323c92b
--- /dev/null
+++ b/fastlane/metadata/android/vi/short_description.txt
@@ -0,0 +1 @@
+Ứng dụng khách di động cho Jellyfin, hệ thống phần mềm miễn phí
diff --git a/fastlane/metadata/android/vi/title.txt b/fastlane/metadata/android/vi/title.txt
new file mode 100644
index 0000000..1ad2520
--- /dev/null
+++ b/fastlane/metadata/android/vi/title.txt
@@ -0,0 +1 @@
+Jellyfin - phương tiện giải trí trong tay của bạn!
diff --git a/fastlane/metadata/android/zh_Hans/changelogs/default.txt b/fastlane/metadata/android/zh_Hans/changelogs/default.txt
new file mode 100644
index 0000000..c509827
--- /dev/null
+++ b/fastlane/metadata/android/zh_Hans/changelogs/default.txt
@@ -0,0 +1,3 @@
+感谢您使用Jellyfin for Android!我们始终建议使用最新版本的Jellyfin服务器。
+
+欲了解更多信息,请查看我们在jellyfin.org发布的博客,或者阅读GitHub上的完整更新日志。
diff --git a/fastlane/metadata/android/zh_Hans/full_description.txt b/fastlane/metadata/android/zh_Hans/full_description.txt
new file mode 100644
index 0000000..a4770c7
--- /dev/null
+++ b/fastlane/metadata/android/zh_Hans/full_description.txt
@@ -0,0 +1,14 @@
+自由管理您的媒体。
+
+Jellyfin项目是一个开源、免费的软件媒体服务器。 没有费用,没有跟踪,没有隐藏的议程。 获取我们的免费服务器,将您的所有音频、视频、照片等内容收集在一个地方。
+
+要使用该应用程序,您必须安装并运行Jellyfin服务器。 在 jellyfin.org 了解更多信息。
+
+借助Jellyfin服务器,您可以:
+
+* 从您的Jellyfin服务器观看直播电视和录制的节目(需要额外的硬件/服务)
+* 串流到您网络上的Chromecast设备
+* 将您的媒体流式传输到您的安卓设备
+* 在易于使用的界面中查看您的收藏
+
+这是适用于 Android 的官方Jellyfin配套应用程序。 感谢您使用Jellyfin!
diff --git a/fastlane/metadata/android/zh_Hans/short_description.txt b/fastlane/metadata/android/zh_Hans/short_description.txt
new file mode 100644
index 0000000..8f87e99
--- /dev/null
+++ b/fastlane/metadata/android/zh_Hans/short_description.txt
@@ -0,0 +1 @@
+免费软件媒体系统Jellyfin的移动客户端
diff --git a/fastlane/metadata/android/zh_Hans/title.txt b/fastlane/metadata/android/zh_Hans/title.txt
new file mode 100644
index 0000000..c8cd3a4
--- /dev/null
+++ b/fastlane/metadata/android/zh_Hans/title.txt
@@ -0,0 +1 @@
+Jellyfin - 您的移动媒体库!
diff --git a/fastlane/metadata/android/zh_Hant/full_description.txt b/fastlane/metadata/android/zh_Hant/full_description.txt
new file mode 100644
index 0000000..4a1c3f9
--- /dev/null
+++ b/fastlane/metadata/android/zh_Hant/full_description.txt
@@ -0,0 +1,14 @@
+自由管理你的多媒體收藏!
+
+Jellyfin 是一個開源、免費的多媒體管理及串流伺服器,它不需任何費用、沒有隱藏的條款、不會收集你的資料。立即使用 jellyfin 收藏、管理你的影片、音樂、相片或其他媒體!
+
+使用此應用程式前,你需要有架設好的 Jellyfin 伺服器。請前往 jellyfin.org 取得更多資訊。
+
+Jellyfin 能讓你:
+
+* 觀看在你的 Jellyfin 伺服器上的直播或預錄的電視節目(需要額外的設備及服務)
+* 串流至本地網絡上的 Chromecast 裝置
+* 串流至 Android 設備
+* 在簡單易用的介面管理你的媒體收藏
+
+此為 Jellyin 官方的 Android 應用程式。感謝你使用 Jellyfin!
diff --git a/fastlane/metadata/android/zh_Hant/short_description.txt b/fastlane/metadata/android/zh_Hant/short_description.txt
new file mode 100644
index 0000000..4a0ac81
--- /dev/null
+++ b/fastlane/metadata/android/zh_Hant/short_description.txt
@@ -0,0 +1 @@
+Jellyfin的手機客戶端版本
diff --git a/fastlane/metadata/android/zh_Hant/title.txt b/fastlane/metadata/android/zh_Hant/title.txt
new file mode 100644
index 0000000..e2dd411
--- /dev/null
+++ b/fastlane/metadata/android/zh_Hant/title.txt
@@ -0,0 +1 @@
+Jellyfin - 你的掌上媒體庫!
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..7a7264b
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,24 @@
+# Project-wide Gradle settings.
+#
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+#
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+#
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=512m
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Allow using snapshot releases of Jellyfin SDK. Possible values are:
+# - "default"
+# - "local" (local Maven repository)
+# - "snapshot" (SDK master)
+# - "unstable-snapshot" (SDK with unstable Jellyfin API)
+sdk.version=default
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..423e592
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,182 @@
+[versions]
+# Plugins
+android-plugin = "8.5.0"
+kotlin = "2.0.0"
+kotlin-ksp = "2.0.0-1.0.22"
+detekt = "1.22.0"
+android-junit5 = "1.10.0.0"
+
+# KotlinX
+coroutines = "1.8.1"
+
+# Core
+koin = "3.5.6"
+koin-compose = "3.5.6"
+androidx-core = "1.13.1"
+androidx-core-splashscreen = "1.0.1"
+androidx-appcompat = "1.7.0"
+androidx-activity = "1.9.0"
+androidx-fragment = "1.8.1"
+androiddesugarlibs = "2.0.4"
+
+# Lifecycle extensions
+androidx-lifecycle = "2.8.3"
+
+# UI
+androidx-constraintlayout = "2.1.4"
+google-material = "1.12.0"
+androidx-webkit = "1.11.0"
+modernandroidpreferences = "2.4.0-beta2"
+
+# Compose
+compose = "1.6.8"
+compose-foundation = "1.6.8"
+compose-material = "1.6.8"
+
+# Network
+jellyfin-sdk = "1.6.8"
+okhttp = "4.12.0"
+coil = "2.6.0"
+cronet-embedded = "119.6045.31"
+
+# Media
+androidx-media = "1.7.0"
+androidx-mediarouter = "1.7.0"
+exoplayer = "2.19.1"
+jellyfin-exoplayer-ffmpegextension = "2.19.1+1"
+playservices = "21.5.0"
+
+# Room
+androidx-room = "2.6.1"
+
+# Monitoring
+timber = "5.0.1"
+leakcanary = "2.14"
+
+# Testing
+junit = "5.10.3"
+kotest = "5.9.1"
+mockk = "1.13.11"
+androidx-test-runner = "1.6.1"
+androidx-test-espresso = "3.6.1"
+
+[plugins]
+android-app = { id = "com.android.application", version.ref = "android-plugin" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
+kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin-ksp" }
+compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+android-junit5 = { id = "de.mannodermaus.android-junit5", version.ref = "android-junit5" }
+detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
+
+[libraries]
+# KotlinX
+coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
+coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
+
+# Core
+koin = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
+koin-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin-compose" }
+android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-plugin" }
+androidx-core = { group = "androidx.core", name = "core", version.ref = "androidx-core" }
+androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidx-core-splashscreen" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
+androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "androidx-activity" }
+androidx-fragment = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "androidx-fragment" }
+androiddesugarlibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androiddesugarlibs" }
+
+# Lifecycle Extensions
+androidx-lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
+androidx-lifecycle-livedata = { group = "androidx.lifecycle", name = "lifecycle-livedata", version.ref = "androidx-lifecycle" }
+androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime", version.ref = "androidx-lifecycle" }
+androidx-lifecycle-common = { group = "androidx.lifecycle", name = "lifecycle-common", version.ref = "androidx-lifecycle" }
+androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "androidx-lifecycle" }
+
+# UI
+google-material = { group = "com.google.android.material", name = "material", version.ref = "google-material" }
+androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" }
+androidx-webkit = { group = "androidx.webkit", name = "webkit", version.ref = "androidx-webkit" }
+modernandroidpreferences = { group = "de.maxr1998", name = "modernandroidpreferences", version.ref = "modernandroidpreferences" }
+
+# Compose
+compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "compose" }
+compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose" }
+compose-animation = { group = "androidx.compose.animation", name = "animation", version.ref = "compose" }
+compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "compose-foundation" }
+compose-material = { group = "androidx.compose.material", name = "material", version.ref = "compose-material" }
+compose-material-icons = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "compose-material" }
+compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "compose-material" }
+
+# Network
+jellyfin-sdk = { group = "org.jellyfin.sdk", name = "jellyfin-core", version.ref = "jellyfin-sdk" }
+okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
+coil = { group = "io.coil-kt", name = "coil-base", version.ref = "coil" }
+cronet-embedded = { group = "org.chromium.net", name = "cronet-embedded", version.ref = "cronet-embedded" }
+
+# Media
+androidx-media = { group = "androidx.media", name = "media", version.ref = "androidx-media" }
+androidx-mediarouter = { group = "androidx.mediarouter", name = "mediarouter", version.ref = "androidx-mediarouter" }
+exoplayer-core = { group = "com.google.android.exoplayer", name = "exoplayer-core", version.ref = "exoplayer" }
+exoplayer-ui = { group = "com.google.android.exoplayer", name = "exoplayer-ui", version.ref = "exoplayer" }
+exoplayer-mediaSession = { group = "com.google.android.exoplayer", name = "extension-mediasession", version.ref = "exoplayer" }
+exoplayer-hls = { group = "com.google.android.exoplayer", name = "exoplayer-hls", version.ref = "exoplayer" }
+exoplayer-cast = { group = "com.google.android.exoplayer", name = "extension-cast", version.ref = "exoplayer" }
+exoplayer-cronet = { group = "com.google.android.exoplayer", name = "extension-cronet", version.ref = "exoplayer" }
+playservices-cast = { group = "com.google.android.gms", name = "play-services-cast", version.ref = "playservices" }
+playservices-castframework = { group = "com.google.android.gms", name = "play-services-cast-framework", version.ref = "playservices" }
+jellyfin-exoplayer-ffmpegextension = { group = "org.jellyfin.exoplayer", name = "exoplayer-ffmpeg-extension", version.ref = "jellyfin-exoplayer-ffmpegextension" }
+
+# Room
+androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidx-room" }
+androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidx-room" }
+
+# Monitoring
+timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
+leakcanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version.ref = "leakcanary" }
+
+# Testing
+junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" }
+junit-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" }
+kotest-runner = { group = "io.kotest", name = "kotest-runner-junit5-jvm", version.ref = "kotest" }
+kotest-assertions = { group = "io.kotest", name = "kotest-assertions-core-jvm", version.ref = "kotest" }
+kotest-property = { group = "io.kotest", name = "kotest-property-jvm", version.ref = "kotest" }
+mockk = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" }
+androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidx-test-runner" }
+androidx-test-espresso = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-test-espresso" }
+
+# Detekt plugins
+detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" }
+
+[bundles]
+coroutines = ["coroutines-core", "coroutines-android"]
+koin = ["koin", "koin-compose"]
+androidx-lifecycle = [
+ "androidx-lifecycle-viewmodel",
+ "androidx-lifecycle-livedata",
+ "androidx-lifecycle-runtime",
+ "androidx-lifecycle-common",
+ "androidx-lifecycle-process",
+]
+compose = [
+ "compose-runtime",
+ "compose-ui",
+ "compose-foundation",
+ "compose-animation",
+ "compose-material",
+ "compose-material-icons",
+ "compose-material-icons-extended",
+]
+exoplayer = [
+ "exoplayer-core",
+ "exoplayer-ui",
+ "exoplayer-mediaSession",
+ "exoplayer-hls",
+ "exoplayer-cronet",
+]
+playservices = [
+ "playservices-cast",
+ "playservices-castframework",
+]
+androidx-room = ["androidx-room-runtime"]
+kotest = ["kotest-runner", "kotest-assertions", "kotest-property"]
+androidx-test = ["androidx-test-runner", "androidx-test-espresso"]
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e644113
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..515ab9d
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,8 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionSha256Sum=f8b4f4772d302c8ff580bc40d0f56e715de69b163546944f787c87abf209c961
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..b740cf1
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..7101f8e
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/renovate.json b/renovate.json
new file mode 100644
index 0000000..0de6413
--- /dev/null
+++ b/renovate.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "github>jellyfin/.github//renovate-presets/gradle",
+ ":dependencyDashboard"
+ ]
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..bb5717c
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,9 @@
+include(":app")
+
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ mavenCentral()
+ google()
+ }
+}