Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
63e86f1
JsonImportTask: delete movies using Room API
UweTrottmann Jul 8, 2026
abe2c9a
ListsTools: move generate list id helper from contract to tools
UweTrottmann Jul 8, 2026
0310db4
Changelog: prepare for next release
UweTrottmann Jul 9, 2026
f68b84e
TmdbTools4: allow mocking by changing from object to class
UweTrottmann Jul 8, 2026
c703c9b
Import: restore original data if importing fails (use db transaction)
UweTrottmann Jul 9, 2026
0d9a81b
Import: for movies with only IMDB ID try to look up TMDB movie
UweTrottmann Jul 8, 2026
88adbdf
Import: display summary, notably list of skipped movies
UweTrottmann Jul 9, 2026
17ce44a
JsonImportTask: support setting tests files for each type
UweTrottmann Jul 9, 2026
39e46ce
Import: support new imdb-movie list item, mapped to TMDB ID on insert
UweTrottmann Jul 9, 2026
a3b3022
Docs: add example for importing shows using TVDB IDs
UweTrottmann Jul 9, 2026
7b782a8
JsonImportTask: include shows and list items, localize summary
UweTrottmann Jul 9, 2026
2a567bb
Import/export: use view model for coroutine scope and export progress
UweTrottmann Jul 10, 2026
2740e66
JsonImportTask: use local format for summary numbers
UweTrottmann Jul 10, 2026
53b301a
Merge branch 'import-movies-with-imdb-ids' into dev
UweTrottmann Jul 10, 2026
9eff69c
Export: use write and truncate mode instead of manually truncating
UweTrottmann Jul 10, 2026
53fc2ac
Prepare version 2026.3.0 (23260300)
UweTrottmann Jul 10, 2026
e114eb5
Changelog: clarify requirements of IMDB ID import, mark 2026.3.0 stable
UweTrottmann Jul 15, 2026
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@
Releases marked with 🧪 (or previously with the "beta" suffix) were released on
[the preview program](https://www.seriesgui.de/help/how-to/basics/preview) only.

## Version 2026.3

* 🔧 Import: support importing movies by IMDB IDs (linked as external ID on TMDB), such as from the TV Time to SeriesGuide transformer.

### 2026.3.0 - 2026-07-10

* 🔧 Import: support importing movies with only IMDB IDs, if it is linked as an external ID on the movie's TMDB entry.
* 🔧 Import: support new "imdb-movie" list item type, maps to added movie using "externalId" as IMDB ID on import, otherwise skips.
* 🔧 Import: display a simple summary of imported and skipped items.
* 🔧 Import: restore original data on failure.
* 🔨 Export: resolve "Illegal seek" error for the DAVx5 WebDAV-backed storage provider and likely others. Thanks @mvanhorn for the tip!

## Version 2026.2

* 🌟 Lists: movies can be added to lists, currently only from the details view.
Expand Down
22 changes: 13 additions & 9 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

```bash
# If it does not exist, create a release branch
git checkout -b releases/2026.2
git checkout -b releases/2026.3

# If it exists, merge latest changes
git checkout releases/2026.2
git checkout releases/2026.3
git merge dev
```

Expand All @@ -27,8 +27,8 @@
- Commit with the suggested message and push

```shell
git commit --all --message "Prepare version 2026.2.0 (23260101)"
git push --set-upstream origin releases/2026.2
git commit --all --message "Prepare version 2026.3.0 (23260101)"
git push --set-upstream origin releases/2026.3
```

- If it does not exist, [create a merge request](https://github.com/UweTrottmann/SeriesGuide/compare/main...) against `main`
Expand All @@ -49,14 +49,18 @@
- Tag release commit

```shell
git tag v2026.2.0
git push origin v2026.2.0
git tag v2026.3.0
git push origin v2026.3.0
git checkout dev
git merge releases/2026.2
git merge releases/2026.3
git push origin dev
```

- Promote to beta channel
- [Create GitHub preview release](https://github.com/UweTrottmann/SeriesGuide/releases/new)
- title like `SeriesGuide 2026.3.0`
- get release notes from [`CHANGELOG.md`](/CHANGELOG.md)
- attach APK
- Create or update preview release post on forum

### Production
Expand All @@ -65,7 +69,7 @@
- Merge release pull request to `main`
- Download universal APK from Play Store
- [Create GitHub release](https://github.com/UweTrottmann/SeriesGuide/releases/new)
- title like `SeriesGuide 2026.2.0`
- title like `SeriesGuide 2026.3.0`
- get release notes from [`CHANGELOG.md`](/CHANGELOG.md)
- attach APK
- Prepare release post on forum
Expand All @@ -77,5 +81,5 @@

```shell
git checkout dev
git merge --no-ff releases/2026.2
git merge --no-ff releases/2026.3
```
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ import com.battlelancer.seriesguide.dataliberation.model.List
import com.battlelancer.seriesguide.dataliberation.model.ListItem
import com.battlelancer.seriesguide.dataliberation.model.Season
import com.battlelancer.seriesguide.dataliberation.model.Show
import com.battlelancer.seriesguide.lists.ListsTools
import com.battlelancer.seriesguide.lists.database.SgList
import com.battlelancer.seriesguide.movies.database.SgMovie
import com.battlelancer.seriesguide.movies.database.toSgMovieForInsert
import com.battlelancer.seriesguide.movies.details.MovieDetails
import com.battlelancer.seriesguide.provider.SeriesGuideContract.ListItemTypes
import com.battlelancer.seriesguide.provider.SeriesGuideContract.ListItems
import com.battlelancer.seriesguide.provider.SeriesGuideContract.Lists
import com.battlelancer.seriesguide.shows.database.SgShow2
import com.battlelancer.seriesguide.shows.episodes.EpisodeFlags
import com.battlelancer.seriesguide.util.tasks.AddListTask
Expand Down Expand Up @@ -57,7 +57,7 @@ class DefaultValuesTest {
tvdb_id = 123456
}
private const val TEST_LIST_NAME = "Test List"
private val TEST_LIST_ID = Lists.generateListId(TEST_LIST_NAME)
private val TEST_LIST_ID = ListsTools.generateListId(TEST_LIST_NAME)!!
private val LIST = List().apply {
name = TEST_LIST_NAME
list_id = TEST_LIST_ID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.battlelancer.seriesguide.R
import com.battlelancer.seriesguide.SgApp
import com.battlelancer.seriesguide.databinding.FragmentAutoBackupBinding
import com.battlelancer.seriesguide.dataliberation.DataLiberationFragment.LiberationResultEvent
import com.battlelancer.seriesguide.dataliberation.JsonExportTask.Export
Expand Down Expand Up @@ -68,10 +67,10 @@ class AutoBackupFragment : Fragment() {

binding.buttonAutoBackupNow.setOnClickListener {
if (TaskManager.tryBackupTask(requireContext())) {
setProgressLock(true)
viewModel.setInProgress(true)
}
}
binding.buttonAutoBackupImport.setOnClickListener { runAutoBackupImport() }
binding.buttonAutoBackupImport.setOnClickListener { viewModel.runImportTask() }

binding.checkBoxAutoBackupCreateCopy.isChecked =
BackupSettings.isCreateCopyOfAutoBackup(requireContext())
Expand Down Expand Up @@ -115,12 +114,13 @@ class AutoBackupFragment : Fragment() {

binding.groupState.visibility = View.GONE
viewModel.updateCopiesFileNames()
setProgressLock(false) // Also disables import button if backup availability unknown.

// restore UI state
if (viewModel.isImportTaskNotCompleted) {
setProgressLock(true)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.isInProgress.collect {
setProgressLock(it)
}
}

viewModel.availableBackupLiveData
.observe(viewLifecycleOwner, { availableBackupTimeString: String? ->
val lastBackupTimeString =
Expand Down Expand Up @@ -157,6 +157,16 @@ class AutoBackupFragment : Fragment() {
}
}
})

viewLifecycleOwner.lifecycleScope.launch {
viewModel.importSummaryState.collect { state ->
state.applyTo(
viewModel,
binding.textViewAutoBackupImportSummary,
binding.scrollViewAutoBackup
)
}
}
}

override fun onStart() {
Expand All @@ -183,6 +193,7 @@ class AutoBackupFragment : Fragment() {
binding = null
}

// This is currently only posted by the export task
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEvent(event: LiberationResultEvent) {
event.handle(view)
Expand All @@ -193,14 +204,7 @@ class AutoBackupFragment : Fragment() {
viewModel.updateAvailableBackupData()
// Note: backup may remove a copy file URI
viewModel.updateCopiesFileNames()
setProgressLock(false)
}

private fun runAutoBackupImport() {
setProgressLock(true)

val importTask = JsonImportTask(requireContext())
viewModel.importTask = SgApp.coroutineScope.launch { importTask.run() }
viewModel.setInProgress(false)
}

private val createShowExportFileResult =
Expand Down Expand Up @@ -235,6 +239,10 @@ class AutoBackupFragment : Fragment() {
binding.buttonAutoBackupImport.isEnabled = isBackupAvailableForImport
}

/**
* Also disables import button if backup availability unknown, so make sure to also call after
* view setup.
*/
private fun setProgressLock(isLocked: Boolean) {
val binding = binding ?: return
binding.buttonAutoBackupNow.isEnabled = !isLocked
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,11 @@ class AutoBackupTask(
.use { source ->
try {
val outFile = context.contentResolver
.openFileDescriptor(outFileUri, "w")
.openFileDescriptor(outFileUri, FILE_MODE)
?: throw AutoBackupException("Unable to open user backup file.")

outFile.use {
FileOutputStream(outFile.fileDescriptor).use {
// Even though using streams and FileOutputStream does not append by
// default, using Storage Access Framework just overwrites existing
// bytes, potentially leaving old bytes hanging over:
// so truncate the file first.
it.channel.truncate(0)
source.copyTo(it)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,21 @@
package com.battlelancer.seriesguide.dataliberation

import android.app.Application
import android.content.Context
import android.text.format.DateUtils
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.battlelancer.seriesguide.R
import com.battlelancer.seriesguide.dataliberation.DataLiberationTools.getFileNameFromUriOrLastPathSegment
import com.battlelancer.seriesguide.dataliberation.JsonExportTask.Export
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch

/**
* View model that checks for available backup files.
*/
class AutoBackupViewModel(application: Application) : AndroidViewModel(application) {
class AutoBackupViewModel(application: Application) : BaseDataLiberationViewModel(application) {

data class CopiesFiles(
val fileNameShows: String?,
Expand All @@ -31,17 +30,6 @@ class AutoBackupViewModel(application: Application) : AndroidViewModel(applicati

val copiesFiles = MutableStateFlow(CopiesFiles("", "", "", "", visible = false))

/**
* Try to keep the import task around on config changes
* so it does not have to be finished.
*/
var importTask: Job? = null
val isImportTaskNotCompleted: Boolean
get() {
val importTask = importTask
return importTask != null && !importTask.isCompleted
}

/** Time string of the available backup, or null if no backup is available. */
val availableBackupLiveData = MutableLiveData<String?>()

Expand Down Expand Up @@ -115,11 +103,9 @@ class AutoBackupViewModel(application: Application) : AndroidViewModel(applicati
}
}

override fun onCleared() {
if (isImportTaskNotCompleted) {
importTask?.cancel(null)
}
importTask = null
fun runImportTask() {
val context: Context = getApplication()
runImportTask { JsonImportTask(context) }
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright © 2026 Uwe Trottmann <uwe@uwetrottmann.com>

package com.battlelancer.seriesguide.dataliberation

import android.app.Application
import android.widget.TextView
import androidx.core.widget.NestedScrollView
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.battlelancer.seriesguide.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

/**
* Supports running an import task, provides state flows for progress and import summary.
*/
open class BaseDataLiberationViewModel(application: Application) : AndroidViewModel(application) {

data class ImportSummaryUiState(
val message: String?,
val isError: Boolean,
val handled: Boolean = false
) {
// This introduces Android UI classes into imports, but is fine as the model is currently
// not used in unit tests.
fun applyTo(
model: BaseDataLiberationViewModel,
textViewSummary: TextView,
scrollView: NestedScrollView
) {
textViewSummary.apply {
setTextAppearance(
if (isError) {
R.style.TextAppearance_SeriesGuide_Body2_Error
} else {
R.style.TextAppearance_SeriesGuide_Body2
}
)
text = message
}
if (!handled) {
model.importSummaryState.update { it.copy(handled = true) }

// Scroll the top of the summary into view
scrollView.post {
scrollView.scrollTo(0, textViewSummary.top)
}
}
}
}

val isInProgress = MutableStateFlow(false)
val importSummaryState = MutableStateFlow(
ImportSummaryUiState(
message = null,
isError = false,
handled = true
)
)

fun setInProgress(value: Boolean) {
isInProgress.value = value
}

fun runImportTask(
jsonImportTask: () -> JsonImportTask
) {
setInProgress(true)

viewModelScope.launch(Dispatchers.Default) {
val result = jsonImportTask().run()

setInProgress(false)
importSummaryState.value = ImportSummaryUiState(
result.message,
result.isError
)
}
}

}
Loading