Skip to content

Repository files navigation

Notice for humans from a human:

I directed an AI agent to create this software. its something i had been looking for but unable to find a way to rip DVDs for my jellyfin server on my phone. Here is what im calling Spindle. it can rip to ISO or MP4 from a USB optical drive. It works for my purposes, which were mostly to find out if it was possible. Everything after this line is AI generated.

Spindle (prototype)

Disc to ISO, straight from your phone. Native Kotlin Android app prototype for imaging the addressable logical blocks exposed by a USB Mass Storage optical drive. (Spindle is a working name: see Branding below.)

Using the app

  • Plug in the drive. Spindle finds it by itself, asks for USB permission, and shows the disc's volume label and size. Plugging the drive in can also offer to open the app.
  • Create ISO image asks where to save; the file name defaults to the disc's own label (for example MY_MOVIE.iso).
  • Eject disc opens the tray (it becomes Close tray while the tray is open). Rescan re-reads the drive.
  • Eject disc when finished (remembered between runs) ejects after a successful image, never after a failed or cancelled one.
  • The disc in the middle is the progress display: its rim fills as imaging proceeds and the hub shows the percentage.
  • Light and dark mode follow the system setting.

Two modes: full ISO or pick titles

A switch under the drive card chooses what the main button does (remembered between runs).

  • Full ISO: copies the whole disc to an .iso, no encoding (the original behaviour, including the optional MP4 afterwards).
  • Pick titles → MP4: Spindle reads the disc's title list (VIDEO_TS.IFO / VTS_nn_0.IFO: title number, length, chapters, audio, size), shows it with checkboxes (the longest title is ticked for you), and rips only what you tick. Each title is read from the drive, CSS-descrambled and piped straight into FFmpeg in one pass. No ISO is made, so nothing waits for the whole disc to copy. One MP4 per title goes into a folder you choose (LABEL.mp4 for a single title, LABEL_title_03.mp4 etc. for several). Titles are done one after another; the progress ring, time left and Cancel cover the whole batch, and a title that already finished is kept if a later one fails or you cancel.
  • Notes: titles shorter than 5 seconds are hidden. For multi-angle discs only the first angle is read, and VOBUs belonging to other interleaved cuts/angles are skipped (DvdStructure.streamTitle). Playback speed is limited by the slower of the drive and the x264 encode, so the drive often idles while the encoder catches up. The drive must stay connected for the whole run.
  • Code: DvdStructure.kt (ISO 9660 + IFO parser, no Android classes), DvdImager.scanTitles() / streamTitle(), DiscTitleSource feeding Mp4Encoder.encodeSource(), and ImagingService.ACTION_RIP.

MP4 conversion (from the Auto DVD Ripper)

Besides title mode, Spindle can turn the movie in an ISO into an .mp4, the job rip.py gives to HandBrakeCLI.

  • Also convert to MP4 (switch, remembered): after you pick the ISO file Spindle asks where the MP4 goes. When imaging finishes, the same background service encodes the main movie from the new ISO. The ISO is kept.
  • Convert an ISO to MP4... picks an ISO already on the phone and an MP4 destination, no drive needed.
  • Progress, time left and Cancel work the same way as for imaging (disc ring, notification). Cancelling or failing removes the half-made MP4.
  • The encode settings come from the Settings screen (gear icon on the main screen). The default preset, "Movie (default)", matches movie_profile.json: x264, constant quality RF 21, preset fast, main profile level 3.1, variable frame rate, AAC audio (384 kb/s for 5.1, 192 kb/s for stereo), no subtitles. See Settings below.
  • How: IsoReader.kt finds the main title in the ISO (the VIDEO_TS title set with the most data), a writer thread streams its VOB files into an FFmpeg pipe, and FFmpeg writes a temporary MP4 in the app cache that is then copied to your file.
  • Not ported from rip.py: the NAS upload, TMDB title lookup and Movie/TV prompt, Blu-ray (MakeMKV), auto-crop, chapter markers, and HandBrake's comb-detecting deinterlacer (Spindle deinterlaces only frames flagged interlaced). The main title is chosen by size rather than by reading the IFO program chains, so a disc whose biggest title set isn't the movie (some TV-episode discs) gives the wrong content.
  • It is software encoding on the phone, so a full movie takes a good while (speed depends on the phone, not tested yet) and the phone gets warm. Keep it plugged in. Free space is needed for the temporary MP4 and the final one.
  • The ISO must be on the phone's own storage (not a cloud provider), since it is read with random access.

Settings

The gear icon on the main screen opens Settings. Changes save immediately and apply to the next file that starts encoding (a file already encoding keeps what it started with).

  • Presets: eight built-in presets modelled on HandBrake's DVD-friendly ones (Movie (default), Very Fast / Fast 480p30, HQ / Super HQ 480p30 Surround, H.265 480p, Phone / small file, Archive), plus your own via Save as.... The preset row shows which preset the current settings match, or "Custom".
  • HandBrake presets: Import... reads a HandBrake preset file (Presets > Export in HandBrake; single preset, preset list or folders) and Export... writes one HandBrake's Import Preset accepts. Importing lists anything that has no equivalent here (two-pass, target size, NLMeans, sharpen, subtitles, passthrough audio, MKV container, auto-crop), and the closest setting is used instead. HandBrake and FFmpeg are different programs, so the same preset will not give byte-identical output.
  • Video: H.264 or H.265, constant quality (RF) or average bitrate, encoder preset, tune, profile, level, extra encoder options (HandBrake's key=value:key=value form), frame rate (source variable/constant, constant, peak).
  • Picture: deinterlace (off, auto by flag, yadif, bwdif, bob), denoise (hqdn3d), square pixels, maximum width/height.
  • Audio: AAC, AC-3, MP3 or Opus, mixdown, stereo and surround bitrates, sample rate, gain, keep all tracks.
  • File: web-optimised (faststart), encoder threads.
  • Discs and ripping: eject when finished, convert ISO to MP4 after imaging, hide titles shorter than N seconds, and the ripped file name pattern ({label}, {title}).
  • A live "FFmpeg command" card shows exactly what the current settings turn into.
  • Code: EncodeSettings.kt (settings, FFmpeg arguments, built-in presets, HandBrake import/export; no Android classes), SettingsStore.kt, SettingsActivity.kt.

Build

Open this folder in Android Studio (Giraffe or newer), allow Gradle sync, then build/run on a phone with USB host support. Android SDK 35 and JDK 17 are expected. The first sync downloads the FFmpeg library; the app builds for arm64-v8a only because that is all the library publishes.

Hardware

  • USB DVD drive using USB Mass Storage Bulk-Only Transport (BOT)
  • USB-C OTG adapter; a powered hub may be necessary
  • Free phone storage at least as large as the drive-reported logical capacity

Background imaging and speed

  • Imaging runs in a foreground service (ImagingService), so it continues with the screen off or the app closed. A notification shows percent, bytes done, current speed and time left, with a Cancel button, and a separate notification reports success or failure at the end. On Android 13+ the app asks for notification permission; imaging still runs if you decline, just without the notification.

  • Speed: the drive is asked for its maximum read speed (SET CD SPEED, best effort), reads start at 16 KiB (the known-safe size) and double up to 128 KiB while the drive copes, falling back if a size hangs or is rejected; each USB data phase is read in one call, and file writes happen on a separate thread so the drive never waits on storage. The drive and USB bridge still set the real ceiling, and it varies across the disc.

  • While imaging, a slider and a "Set speed" button change the drive's read speed live (top position = maximum). The request is sent between two reads; the status shows whether the drive accepted it and what speed it now reports.

Important limitations

  • This is an experimental prototype, not a tested release.
  • Supports only a Mass Storage interface with bulk IN/OUT endpoints and 2048-byte logical blocks.
  • Reads the full logical capacity reported by SCSI READ CAPACITY(10); this may include unreadable/unrecorded areas and is not a forensic physical-disc image.
  • CSS-scrambled DVD-Video discs are authenticated with the drive and descrambled while imaging, so the ISO is decrypted (Css.kt). Only libdvdcss's "player key" method is ported; its disc-key brute force and title-key cracking fallbacks are not. If the drive won't authenticate (for example an RPC-II drive with no region set, or one set to a different region than the disc), imaging stops with an error that reports the drive's region state.
  • Does not handle CPPM/CPRM, Blu-ray AACS, or other copy protection.
  • Basic Bulk-Only reset recovery and sector retry only. No multi-LUN selection and no resume of an interrupted image yet.
  • Unreadable sectors are retried, then zero-filled and counted (the result message shows how many).
  • Test first with a non-protected data DVD, then a CSS disc. Do not rely on it for archival work.

Branding

  • Name: res/values/strings.xml (app_name, tagline). The launcher label comes from there.
  • Palette: res/values/colors.xml and res/values-night/colors.xml. Green and gold come from the dye layers of a recordable disc: brand_primary, brand_gold, icon_background.
  • Icon: adaptive vector icon (drawable/ic_launcher_foreground.xml, a themed monochrome version for Android 13+, and mipmap-anydpi-v26/). The notification icon is drawable/ic_stat_spindle.xml; the in-app logo is drawable/ic_logo_mark.xml.
  • brand/spindle-icon-512.png is a 512 px render of the icon for store listings.
  • The app id (applicationId and namespace) is com.tsl.spindle. Still to do before sharing the app: check that the name "Spindle" is free to use.

Legal notice

Spindle is meant for making personal backups of discs you own. Circumventing copy protection (CSS here) is restricted in some countries even for personal copies, for example under the US DMCA; you are responsible for following the law where you live. Nothing here is legal advice.

License

Spindle is free software under the GNU General Public License v3.0; see LICENSE. Third-party components and credits are in NOTICE.md.

Css.kt is a Kotlin port of parts of libdvdcss (GPL v2 or later, (C) VideoLAN and contributors), so this app is a derivative work and must be distributed under the GPL if you share it. MP4 conversion adds the GPL-3.0 build of FFmpeg (ffmpeg-kit-full-gpl, with x264) from the community-maintained FFmpegKit continuation (dev.ffmpegkit-maintained; the original FFmpegKit was retired). Together that means the combined app is distributed under GPL v3. Circumventing CSS is restricted in some countries (for example under the US DMCA) even for personal copies; check your local law.

Troubleshooting

If the drive does not appear, use a powered USB-C hub and tap Refresh USB devices. Some Android USB stacks or drive bridges may not expose the expected Mass Storage interface.

About

Rip DVDs with your Android phone and a USB optical drive.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages