Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Release

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- name: set up JDK 19
uses: actions/setup-java@v4
with:
java-version: '19'
distribution: 'temurin'
cache: gradle

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Decode Keystore
if: github.ref == 'refs/heads/main'
env:
RELEASE_KEYSTORE_BASE64: ${{ secrets.RELEASE_KEYSTORE_BASE64 }}
run: |
if [ -n "$RELEASE_KEYSTORE_BASE64" ]; then
echo "$RELEASE_KEYSTORE_BASE64" | tr -d '\r\n ' | base64 -d -i > release.keystore
fi

- name: Build with Gradle
env:
RELEASE_KEYSTORE_PATH: ../release.keystore
RELEASE_KEYSTORE_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }}
RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }}
RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }}
run: ./gradlew assembleRelease

- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: odesli-android
path: odesli/build/outputs/apk/release/*.apk

- name: Get version from CHANGELOG
id: changelog
uses: mindsers/changelog-reader-action@v2
with:
validation_level: warn

- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true

- name: Create Release
if: github.ref == 'refs/heads/main'
uses: ncipollo/release-action@v1
with:
tag: ${{ steps.changelog.outputs.version }}
name: Release ${{ steps.changelog.outputs.version }}
body: ${{ steps.changelog.outputs.changes }}
artifacts: "artifacts/*.apk"
token: ${{ secrets.GITHUB_TOKEN }}
allowUpdates: true
artifactErrorsFailBuild: true
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@
.cxx
local.properties
/app/release

# Keystore files
*.keystore
*.jks
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [v1.2.0]

- Added: Fallback metadata for "Odesli Page" output
- Fixed: Input fields layout overflow
- Fixed: Main content scrolling
- Fixed: FAB blocking bottom content
- Fixed: Settings text cutoff
- Fixed: Result card text alignment

## [v1.1.3]

- Fixed: Missing song information (title, artist, artwork) when "Odesli Page" is selected

## [v1.1.2]

- Improvement: Better feedback message when "Auto Copy Link" copies a link ("Link copied")

## [v1.1.1]

- Fixed: Share to "Odesli Page" failing when "Auto Copy Link" is off
- Fixed: App hanging when Odesli API fails or returns no data
- Added: Feedback toast when "Auto Copy Link" successfully copies a link

## [v1.1.0]

- Add support for converting links to an Odesli URL

## [v1.0.0]

- Initial Release
- Support for converting between streaming services
- Automatically copy links to clipboard
98 changes: 74 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,93 @@
# odesli-android
A simple app that uses the Odesli API to convert songs/albums to many streaming services\

A simple app that uses the Odesli API to convert songs/albums to many streaming services\
[Download](https://github.com/prochy-exe/odesli-android/releases/latest)

# Compatibility
Thanks to the simplicity of this app, it supports all types of devices within the one APK.

Thanks to the simplicity of this app, it supports all types of devices within the one APK.

# How does it work?
This app uses the Odesli API to convert links to any streaming service of your choosing.\

This app uses the Odesli API to convert links to any streaming service of your choosing.\
Generally speaking this conversion process lasts max 10 seconds.\
The request to [Odesli API](https://odesli.co/) is sent using the [Retrofit library](https://github.com/square/retrofit), the [Coil library](https://coil-kt.github.io/coil/) is responsible for asynchronously rendering the song/album cover.\
The Odesli API also has support for country codes to make sure the song or album you are trying to convert will be available for you. That means that the app has to figure out your country in some capacity, however as you might have noticed, I have decided to not ask for location directly and obtain the country code using other means which will be explained in the next paragraph.\
This app is can be used in 2 major ways:
* By copying and converting the link directly into the app
* By sharing the link to the app\
This option allows you to easily and quickly share the song/album you want to convert to another streaming service.\
Behavior of this option is customizable via the settings
* Preferred service\

- By copying and converting the link directly into the app
- By sharing the link to the app\
This option allows you to easily and quickly share the song/album you want to convert to another streaming service.\
Behavior of this option is customizable via the settings
- Preferred service\
This service will be used when sharing a song or an album into the app
* Auto-copy\
- Auto-copy\
This option will automatically copy the link into your clipboard without showing the UI
* Show card when the song or album wasn't found on the preferred service\
This option will allow you to pick a different service right away, otherwise a toast will be shown informing you that the preferred service doesn't have the song or album you are trying to convert
- Show card when the song or album wasn't found on the preferred service\
This option will allow you to pick a different service right away, otherwise a toast will be shown informing you that the preferred service doesn't have the song or album you are trying to convert

# Obtaining your country code
Instead of relying on your location which might feel unnecessary, and it totally is, I decide to use the TelephonyService, provided by Android, for devices with a SIM card, and other devices will make a request to [IPinfo](https://ipinfo.io), which is a 3rd party service that allows obtaining basic location information about you.\
**!! I DO NOT GATHER THIS INFORMATION, IT'S BEING DIRECTLY PASSED TO THE ODESLI API !!**

Instead of relying on your location which might feel unnecessary, and it totally is, I decide to use the TelephonyService, provided by Android, for devices with a SIM card, and other devices will make a request to [IPinfo](https://ipinfo.io), which is a 3rd party service that allows obtaining basic location information about you.\
**!! I DO NOT GATHER THIS INFORMATION, IT'S BEING DIRECTLY PASSED TO THE ODESLI API !!**

# Screenshots

![Screenshots](./github_assets/screenshots.png "Screenshots")

Left to right:
* Main UI
* Settings popup
* Main UI convert
* Share UI
![Screenshots](./github_assets/screenshots.png "Screenshots")

Left to right:

- Main UI
- Settings popup
- Main UI convert
- Share UI

# Libraries/APIs used
* [Odesli/Songlink](https://odesli.co/)
* [Coil](https://coil-kt.github.io/coil/)
* [Retrofit](https://github.com/square/retrofit)
* [IPinfo](https://ipinfo.io)

- [Odesli/Songlink](https://odesli.co/)
- [Coil](https://coil-kt.github.io/coil/)
- [Retrofit](https://github.com/square/retrofit)
- [IPinfo](https://ipinfo.io)

# Building and Signing

To sign your builds, you'll need to generate a keystore file and configure your repository secrets to allow GitHub Actions to sign your releases.

### Local Builds

To build signed releases locally, you need to generate a keystore file:

1. Generate the keystore (ensure Java/JDK is installed):

```bash
keytool -genkey -v -keystore odesli/release.keystore -alias odesli -keyalg RSA -keysize 2048 -validity 10000 -storepass android -keypass android -dname "CN=Odesli Android, OU=Mobile, O=PixP, L=Mountain View, S=California, C=US"
```

_Note: Place the `release.keystore` file inside the `odesli/` directory._

2. Build the release APK:
```bash
./gradlew assembleRelease
```
The build script will automatically detect the `odesli/release.keystore` file.

### CI/CD (GitHub Actions)

You'll need your local signature file to generate the base64 encoded content of your keystore file.

For GitHub Actions to build signed APKs, you must configure the following **Repository Secrets** (Settings > Secrets and variables > Actions):

1. **`RELEASE_KEYSTORE_BASE64`**: The base64 encoded content of your keystore file.
Generate this by running:

```bash
base64 -i odesli/release.keystore -o -
```

(Copy the output string)

2. **`RELEASE_KEYSTORE_PASSWORD`**: `android`
3. **`RELEASE_KEY_ALIAS`**: `odesli`
4. **`RELEASE_KEY_PASSWORD`**: `android`

The workflow will decode the secret into a file during the build process, ensuring your key remains secure.
Empty file modified gradlew
100644 → 100755
Empty file.
32 changes: 28 additions & 4 deletions odesli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,52 @@ android {
namespace = "com.prochy.odesliandroid"
compileSdk = 34

buildToolsVersion = "34.0.0"

defaultConfig {
applicationId = "com.prochy.odesliandroid"
minSdk = 21
targetSdk = 34
versionCode = 1
versionName = "1.0"
versionCode = 4
versionName = "1.2.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}

signingConfigs {
create("release") {
val keystoreFile = System.getenv("RELEASE_KEYSTORE_PATH")?.let { file(it) } ?: file("release.keystore")
val keystorePassword = (System.getenv("RELEASE_KEYSTORE_PASSWORD") ?: "android").trim()
val keyAlias = (System.getenv("RELEASE_KEY_ALIAS") ?: "odesli").trim()
val keyPassword = (System.getenv("RELEASE_KEY_PASSWORD") ?: "android").trim()

if (keystoreFile.exists() && keystoreFile.length() > 0) {
storeFile = keystoreFile
storePassword = keystorePassword
this.keyAlias = keyAlias
this.keyPassword = keyPassword
} else {
println("Signing Config: Release keystore not found or empty. Falling back to debug signing.")
}
}
}

buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
val releaseSigning = signingConfigs.getByName("release")
signingConfig = if (releaseSigning.storeFile != null && releaseSigning.storeFile!!.exists()) {
releaseSigning
} else {
signingConfigs.getByName("debug")
}
}
debug {
isMinifyEnabled = false
Expand All @@ -40,7 +65,6 @@ android {
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_19
sourceCompatibility = JavaVersion.VERSION_19
targetCompatibility = JavaVersion.VERSION_19
}
Expand Down
Loading