Skip to content

Commit d1a0cad

Browse files
authored
Merge pull request #110 from jessielw/dev
Dev
2 parents aced498 + 6fc0939 commit d1a0cad

13 files changed

Lines changed: 298 additions & 73 deletions

.github/workflows/docker.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ on:
1010
description: "Image tag to push (e.g. 0.1.0-beta1)"
1111
required: true
1212
type: string
13+
ref:
14+
description: "Branch or commit to build from (e.g. dev)"
15+
required: false
16+
default: dev
17+
type: string
1318

1419
concurrency:
1520
group: docker-${{ github.ref }}
@@ -30,6 +35,8 @@ jobs:
3035
steps:
3136
- name: Checkout
3237
uses: actions/checkout@v4
38+
with:
39+
ref: ${{ github.event.inputs.ref || github.ref }}
3340

3441
- name: Set up QEMU
3542
uses: docker/setup-qemu-action@v3

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.1.0-beta.13] - 2026-04-25
9+
10+
### Fixed
11+
12+
- Migration issue that could happen when jumping several version at once
13+
814
## [0.1.0-beta.12] - 2026-04-24
915

1016
### Added

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,20 @@ I needed a solution to manage a single physical media library while utilizing bo
2424

2525
Driven by the increasing cost of storage and the need to reclaim used disk space, inspired by Maintainerr, **Reclaimerr** was born.
2626

27+
## Star History
28+
29+
<a href="https://www.star-history.com/?repos=jessielw%2FReclaimerr&type=date&legend=top-left">
30+
<picture>
31+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=jessielw/Reclaimerr&type=date&theme=dark&legend=top-left" />
32+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=jessielw/Reclaimerr&type=date&legend=top-left" />
33+
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=jessielw/Reclaimerr&type=date&legend=top-left" />
34+
</picture>
35+
</a>
36+
2737
# Discussions
2838

2939
While I prefer we utilize github's [discussions](https://github.com/jessielw/Reclaimerr/discussions) for historical purposes, I've had quite a few users ask for _Discord_. I personally am getting away from Discord ASAP, but I did create a public matrix server for discussions and some _support_. Feel free to join https://matrix.to/#/#reclaimerr:matrix.org!
3040

31-
3241
# AI Disclosures
3342

3443
AI (LLMs) are _everywhere_ and _everyone_ is using them. I understand that many users are concerned about projects heavily generated by AI or lacking a personal touch. Reclaimerr was built from the ground up and was **not** generated using LLMs or a fork of any other project.

backend/alembic/versions/1b25d7fd62d3_nullable_year.py

Lines changed: 104 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,115 @@
1010
from alembic import op
1111
import sqlalchemy as sa
1212

13-
14-
# revision identifiers, used by Alembic.
1513
revision: str = '1b25d7fd62d3'
1614
down_revision: Union[str, None] = 'e85082c4be59'
1715
branch_labels: Union[str, Sequence[str], None] = None
1816
depends_on: Union[str, Sequence[str], None] = None
1917

2018

19+
def _movies_table(year_nullable: bool) -> sa.Table:
20+
return sa.Table(
21+
'movies', sa.MetaData(),
22+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
23+
sa.Column('title', sa.String(length=512), nullable=False),
24+
sa.Column('tmdb_id', sa.Integer(), nullable=False),
25+
sa.Column('year', sa.SmallInteger(), nullable=year_nullable),
26+
sa.Column('size', sa.Integer(), nullable=True),
27+
sa.Column('radarr_id', sa.Integer(), nullable=True),
28+
sa.Column('imdb_id', sa.String(length=20), nullable=True),
29+
sa.Column('tmdb_title', sa.String(length=512), nullable=True),
30+
sa.Column('original_title', sa.String(length=512), nullable=True),
31+
sa.Column('tmdb_release_date', sa.DateTime(), nullable=True),
32+
sa.Column('original_language', sa.String(length=10), nullable=True),
33+
sa.Column('homepage', sa.String(length=500), nullable=True),
34+
sa.Column('origin_country', sa.JSON(), nullable=True),
35+
sa.Column('poster_url', sa.String(length=500), nullable=True),
36+
sa.Column('backdrop_url', sa.String(length=500), nullable=True),
37+
sa.Column('overview', sa.Text(), nullable=True),
38+
sa.Column('genres', sa.JSON(), nullable=True),
39+
sa.Column('popularity', sa.Float(), nullable=True),
40+
sa.Column('vote_average', sa.Float(), nullable=True),
41+
sa.Column('vote_count', sa.Integer(), nullable=True),
42+
sa.Column('revenue', sa.Integer(), nullable=True),
43+
sa.Column('runtime', sa.Integer(), nullable=True),
44+
sa.Column('status', sa.String(length=50), nullable=True),
45+
sa.Column('tagline', sa.String(length=255), nullable=True),
46+
sa.Column('last_viewed_at', sa.DateTime(), nullable=True),
47+
sa.Column('view_count', sa.Integer(), nullable=False),
48+
sa.Column('never_watched', sa.Boolean(), nullable=False, server_default='1'),
49+
sa.Column('added_at', sa.DateTime(), nullable=True),
50+
sa.Column('removed_at', sa.DateTime(), nullable=True),
51+
sa.Column('last_metadata_refresh_at', sa.DateTime(), nullable=True),
52+
sa.PrimaryKeyConstraint('id'),
53+
sa.UniqueConstraint('tmdb_id'),
54+
sa.UniqueConstraint('radarr_id'),
55+
sa.UniqueConstraint('imdb_id'),
56+
sa.Index('ix_movies_tmdb_id', 'tmdb_id'),
57+
sa.Index('ix_movies_radarr_id', 'radarr_id'),
58+
sa.Index('ix_movies_imdb_id', 'imdb_id'),
59+
)
60+
61+
62+
def _series_table(year_nullable: bool) -> sa.Table:
63+
return sa.Table(
64+
'series', sa.MetaData(),
65+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
66+
sa.Column('title', sa.String(length=512), nullable=False),
67+
sa.Column('tmdb_id', sa.Integer(), nullable=False),
68+
sa.Column('year', sa.SmallInteger(), nullable=year_nullable),
69+
sa.Column('size', sa.Integer(), nullable=True),
70+
sa.Column('sonarr_id', sa.Integer(), nullable=True),
71+
sa.Column('imdb_id', sa.String(length=20), nullable=True),
72+
sa.Column('tvdb_id', sa.String(length=20), nullable=True),
73+
sa.Column('tmdb_title', sa.String(length=512), nullable=True),
74+
sa.Column('original_title', sa.String(length=512), nullable=True),
75+
sa.Column('tmdb_first_air_date', sa.DateTime(), nullable=True),
76+
sa.Column('tmdb_last_air_date', sa.DateTime(), nullable=True),
77+
sa.Column('original_language', sa.String(length=10), nullable=True),
78+
sa.Column('homepage', sa.String(length=500), nullable=True),
79+
sa.Column('origin_country', sa.JSON(), nullable=True),
80+
sa.Column('poster_url', sa.String(length=500), nullable=True),
81+
sa.Column('backdrop_url', sa.String(length=500), nullable=True),
82+
sa.Column('overview', sa.Text(), nullable=True),
83+
sa.Column('genres', sa.JSON(), nullable=True),
84+
sa.Column('popularity', sa.Float(), nullable=True),
85+
sa.Column('vote_average', sa.Float(), nullable=True),
86+
sa.Column('vote_count', sa.Integer(), nullable=True),
87+
sa.Column('status', sa.String(length=50), nullable=True),
88+
sa.Column('tagline', sa.String(length=255), nullable=True),
89+
sa.Column('season_count', sa.Integer(), nullable=True),
90+
sa.Column('last_viewed_at', sa.DateTime(), nullable=True),
91+
sa.Column('view_count', sa.Integer(), nullable=False),
92+
sa.Column('never_watched', sa.Boolean(), nullable=False, server_default='1'),
93+
sa.Column('added_at', sa.DateTime(), nullable=True),
94+
sa.Column('removed_at', sa.DateTime(), nullable=True),
95+
sa.Column('last_metadata_refresh_at', sa.DateTime(), nullable=True),
96+
sa.PrimaryKeyConstraint('id'),
97+
sa.UniqueConstraint('tmdb_id'),
98+
sa.UniqueConstraint('sonarr_id'),
99+
sa.UniqueConstraint('imdb_id'),
100+
sa.UniqueConstraint('tvdb_id'),
101+
sa.Index('ix_series_tmdb_id', 'tmdb_id'),
102+
sa.Index('ix_series_sonarr_id', 'sonarr_id'),
103+
sa.Index('ix_series_imdb_id', 'imdb_id'),
104+
sa.Index('ix_series_tvdb_id', 'tvdb_id'),
105+
)
106+
107+
21108
def upgrade() -> None:
22109
conn = op.get_bind()
23-
# drop any orphaned Alembic temp tables left by a previously interrupted migration.
24110
for tbl in ('_alembic_tmp_movies', '_alembic_tmp_series'):
25111
conn.execute(sa.text(f'DROP TABLE IF EXISTS "{tbl}"'))
26112

27-
# SQLite batch_alter_table recreates the table (copy → drop original → rename).
28-
# The DROP TABLE fails if FK enforcement is on because child tables reference it.
29113
conn.execute(sa.text('PRAGMA foreign_keys=OFF'))
30114
try:
31-
with op.batch_alter_table('movies', schema=None) as batch_op:
32-
batch_op.alter_column('year',
33-
existing_type=sa.SMALLINT(),
34-
nullable=True)
35-
36-
with op.batch_alter_table('series', schema=None) as batch_op:
37-
batch_op.alter_column('year',
38-
existing_type=sa.SMALLINT(),
39-
nullable=True)
115+
with op.batch_alter_table('movies', recreate='always',
116+
copy_from=_movies_table(year_nullable=False)) as batch_op:
117+
batch_op.alter_column('year', existing_type=sa.SMALLINT(), nullable=True)
118+
119+
with op.batch_alter_table('series', recreate='always',
120+
copy_from=_series_table(year_nullable=False)) as batch_op:
121+
batch_op.alter_column('year', existing_type=sa.SMALLINT(), nullable=True)
40122
finally:
41123
conn.execute(sa.text('PRAGMA foreign_keys=ON'))
42124

@@ -45,14 +127,12 @@ def downgrade() -> None:
45127
conn = op.get_bind()
46128
conn.execute(sa.text('PRAGMA foreign_keys=OFF'))
47129
try:
48-
with op.batch_alter_table('series', schema=None) as batch_op:
49-
batch_op.alter_column('year',
50-
existing_type=sa.SMALLINT(),
51-
nullable=False)
52-
53-
with op.batch_alter_table('movies', schema=None) as batch_op:
54-
batch_op.alter_column('year',
55-
existing_type=sa.SMALLINT(),
56-
nullable=False)
130+
with op.batch_alter_table('series', recreate='always',
131+
copy_from=_series_table(year_nullable=True)) as batch_op:
132+
batch_op.alter_column('year', existing_type=sa.SMALLINT(), nullable=False)
133+
134+
with op.batch_alter_table('movies', recreate='always',
135+
copy_from=_movies_table(year_nullable=True)) as batch_op:
136+
batch_op.alter_column('year', existing_type=sa.SMALLINT(), nullable=False)
57137
finally:
58-
conn.execute(sa.text('PRAGMA foreign_keys=ON'))
138+
conn.execute(sa.text('PRAGMA foreign_keys=ON'))

backend/alembic/versions/20666951d75d_add_emby_support_and_emby_season_id_to_.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,27 @@
1010
from alembic import op
1111
import sqlalchemy as sa
1212

13-
14-
# revision identifiers, used by Alembic.
1513
revision: str = '20666951d75d'
1614
down_revision: Union[str, None] = '1b25d7fd62d3'
1715
branch_labels: Union[str, Sequence[str], None] = None
1816
depends_on: Union[str, Sequence[str], None] = None
1917

2018

2119
def upgrade() -> None:
22-
# ### commands auto generated by Alembic - please adjust! ###
23-
with op.batch_alter_table('seasons', schema=None) as batch_op:
24-
batch_op.add_column(sa.Column('emby_season_id', sa.String(length=100), nullable=True))
25-
26-
# ### end Alembic commands ###
20+
conn = op.get_bind()
21+
inspector = sa.inspect(conn)
22+
existing_cols = [c['name'] for c in inspector.get_columns('seasons')]
23+
if 'emby_season_id' not in existing_cols:
24+
with op.batch_alter_table('seasons', schema=None) as batch_op:
25+
batch_op.add_column(
26+
sa.Column('emby_season_id', sa.String(length=100), nullable=True)
27+
)
2728

2829

2930
def downgrade() -> None:
30-
# ### commands auto generated by Alembic - please adjust! ###
31-
with op.batch_alter_table('seasons', schema=None) as batch_op:
32-
batch_op.drop_column('emby_season_id')
33-
34-
# ### end Alembic commands ###
31+
conn = op.get_bind()
32+
inspector = sa.inspect(conn)
33+
existing_cols = [c['name'] for c in inspector.get_columns('seasons')]
34+
if 'emby_season_id' in existing_cols:
35+
with op.batch_alter_table('seasons', schema=None) as batch_op:
36+
batch_op.drop_column('emby_season_id')

0 commit comments

Comments
 (0)