Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a0a6a3d
chore: migrate "add new studio" lookup and add flow to tanstack query…
Mothball7205 Jun 19, 2026
1a414a1
Chore: Bump postcss from 8.5.10 to 8.5.11 (#299)
dependabot[bot] Jun 19, 2026
c529a10
Chore: Bump ghcr.io/devcontainers/features/node from 1.7.1 to 2.1.0 (…
dependabot[bot] Jun 23, 2026
04f2b83
chore: bump swashbuckle (#321)
plz12345 Jul 12, 2026
0736790
Chore: Cloudflare cookie test expiring and reddening CI (#325)
plz12345 Jul 15, 2026
ae2248f
test: add MountCheck coverage
plz12345 Jul 15, 2026
c2e4645
fix: cache the mount list instead of enumerating it per lookup
plz12345 Jul 15, 2026
7ec3325
fix: stop re-querying root folders once per movie
plz12345 Jul 15, 2026
09f0744
perf: walk the path once per mount lookup, not once per mount
plz12345 Jul 15, 2026
dd1c29e
chore: address SonarQube findings in disk provider
plz12345 Jul 15, 2026
7a66a5f
fix: multi-brand studio token parsing for cross-posted scenes (#320)
v8eta Jul 16, 2026
3018588
Fixed: Movie file quality missing from the paged movie/scene index
plz12345 Jul 16, 2026
cfdf27d
Fixed: Unbounded pageSize on the paged movie endpoint
plz12345 Jul 16, 2026
154f63d
Fixed: Scene title replacement eating codec tokens from simpleRelease…
plz12345 Jul 16, 2026
1ef2ed2
fix: Check Health taking 24h+ on large libraries (#324)
plz12345 Jul 17, 2026
d027b4f
Fixed: Movie file quality missing from the paged movie/scene index (#…
plz12345 Jul 17, 2026
f11a731
Chore: Bump ws from 7.5.10 to 7.5.12 (#328)
dependabot[bot] Jul 18, 2026
954c64a
Chore: Bump qs from 6.15.1 to 6.15.2 (#292)
dependabot[bot] Jul 18, 2026
00d5522
Chore: Bump @babel/plugin-transform-modules-systemjs from 7.29.0 to 7…
dependabot[bot] Jul 18, 2026
f8f0faf
Chore: Bump postcss from 8.5.11 to 8.5.12 (#318)
dependabot[bot] Jul 18, 2026
39be234
Chore: Bump @babel/core from 7.29.0 to 7.29.6 (#311)
dependabot[bot] Jul 18, 2026
874970f
Chore: Bump js-yaml from 4.1.1 to 4.3.0 (#319)
dependabot[bot] Jul 18, 2026
39b1db6
Chore: derive base release version from git tag instead of hardcoded …
plz12345 Jul 18, 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
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"name": "Whisparr",
"image": "mcr.microsoft.com/devcontainers/dotnet:1-8.0",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"ghcr.io/devcontainers/features/node:2": {
"nodeGypDependencies": true,
"version": "20",
"nvmVersion": "latest"
Expand Down
5 changes: 4 additions & 1 deletion .github/actions/build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ inputs:
version:
description: "Version number to build"
required: true
assembly_version:
description: "Assembly version (major.minor.patch.build) to stamp"
required: true
framework:
description: ".net framework used for the build"
required: true
Expand Down Expand Up @@ -38,7 +41,7 @@ runs:
echo "SDK_PATH=${{ env.DOTNET_ROOT }}/sdk/${DOTNET_VERSION}" >> "$GITHUB_ENV"
echo "WHISPARR_VERSION=${{ inputs.version }}" >> "$GITHUB_ENV"
echo "BRANCH=${{ inputs.branch }}" >> "$GITHUB_ENV"
echo "WHISPARR_ASSEMBLY_VERSION=${{ env.WHISPARR_ASSEMBLY_VERSION }}" >> "$GITHUB_ENV"
echo "WHISPARR_ASSEMBLY_VERSION=${{ inputs.assembly_version }}" >> "$GITHUB_ENV"

if [ "$RUNNER_OS" == "Windows" ]; then
echo "NUGET_PACKAGES=D:\nuget\packages" >> "$GITHUB_ENV"
Expand Down
34 changes: 31 additions & 3 deletions .github/workflows/build_v3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ env:
FRAMEWORK: net10.0
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
WHISPARR_MAJOR_VERSION: 3
VERSION: 3.3.4
WHISPARR_ASSEMBLY_VERSION: 3.3.4.${{ github.run_number }}
# Base version now comes from the newest base tag (vX.Y.Z) reachable from the
# building branch; this is only the fallback for the seed window before any
# base tag exists. See prepare job below.
VERSION_FALLBACK: 3.3.4

jobs:
prepare:
Expand All @@ -34,20 +36,45 @@ jobs:
framework: ${{ steps.variables.outputs.framework }}
major_version: ${{ steps.variables.outputs.major_version }}
version: ${{ steps.variables.outputs.version }}
assembly_version: ${{ steps.variables.outputs.assembly_version }}
branch: ${{ steps.variables.outputs.branch }}
steps:
- name: Check out
uses: actions/checkout@v5
with:
fetch-depth: 0 # fetches tags; needed to read the version marker

- name: Setup Environment Variables
id: variables
shell: bash
run: |
# Base version = highest base tag (strictly vX.Y.Z) reachable from THIS
# branch's history. --merged keeps it reachability-scoped so eros-develop
# and eros each resolve their own base; the strict regex rejects both the
# CI-minted -develop.N/-release.N tags and the legacy 4-part vX.Y.Z.B tags
# inherited from upstream (which glob-based matching cannot separate).
# `|| true` keeps the default bash `-eo pipefail` shell from aborting the
# step when no base tag matches yet (seed window) or when `head` closes the
# pipe early (SIGPIPE).
BASE_TAG=$(git tag --merged HEAD --sort=-v:refname 2>/dev/null \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || true)
if [ -n "$BASE_TAG" ]; then
BASE="${BASE_TAG#v}"
echo "Base version ${BASE} (from tag ${BASE_TAG})"
else
BASE="${{ env.VERSION_FALLBACK }}"
echo "Base version ${BASE} (from VERSION_FALLBACK; no vX.Y.Z tag reachable)"
fi

if [[ "${{ github.ref_name }}" == "eros-develop" ]]; then
SEMVER_LABEL="develop"
else
SEMVER_LABEL="release"
fi
echo "framework=${{ env.FRAMEWORK }}" >> "$GITHUB_OUTPUT"
echo "major_version=${{ env.WHISPARR_MAJOR_VERSION }}" >> "$GITHUB_OUTPUT"
echo "version=${{ env.VERSION }}-${SEMVER_LABEL}.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
echo "version=${BASE}-${SEMVER_LABEL}.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
echo "assembly_version=${BASE}.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
echo "branch=${RAW_BRANCH_NAME//\//-}" >> "$GITHUB_OUTPUT"

backend:
Expand Down Expand Up @@ -99,6 +126,7 @@ jobs:
with:
branch: ${{ needs.prepare.outputs.branch }}
version: ${{ needs.prepare.outputs.version }}
assembly_version: ${{ needs.prepare.outputs.assembly_version }}
framework: ${{ needs.prepare.outputs.framework }}
runtime: ${{ matrix.runtime }}
package_tests: ${{ matrix.package_tests }}
Expand Down
122 changes: 1 addition & 121 deletions frontend/src/Store/Actions/addMovieActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import createAjaxRequest from 'Utilities/createAjaxRequest';
import getNewPerformer from 'Utilities/Performer/getNewPerformer';
import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState';
import getNewStudio from 'Utilities/Studio/getNewStudio';
import { set, update, updateItem } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer';
Expand All @@ -28,8 +27,6 @@ export const defaultState = {
isAdded: false,
addError: null,
items: [],
studiosWithStatus: [],

movieDefaults: {
rootFolderPath: '',
monitor: 'movieOnly',
Expand Down Expand Up @@ -65,31 +62,24 @@ export const persistState = [
//
// Actions Types

export const LOOKUP_STUDIO = 'addMovie/lookupStudio';
export const LOOKUP_PERFORMER = 'addMovie/lookupPerformer';
export const ADD_PERFORMER = 'addMovie/addPerformer';
export const ADD_STUDIO = 'addMovie/addStudio';
export const SET_ADD_MOVIE_VALUE = 'addMovie/setAddMovieValue';
export const SET_ADD_PERFORMER_VALUE = 'addMovie/setAddPerformerValue';
export const SET_ADD_STUDIO_VALUE = 'addMovie/setAddStudioValue';
export const CLEAR_ADD_MOVIE = 'addMovie/clearAddMovie';
export const SET_ADD_MOVIE_DEFAULT = 'addMovie/setAddMovieDefault';
export const SET_ADD_PERFORMER_DEFAULT = 'addMovie/setAddPerformerDefault';
export const SET_ADD_STUDIO_DEFAULT = 'addMovie/setAddStudioDefault';
export const SET_STUDIOS_WITH_STATUS = 'addMovie/setStudiosWithStatus';

//
// Action Creators

export const lookupStudio = createThunk(LOOKUP_STUDIO);
export const lookupPerformer = createThunk(LOOKUP_PERFORMER);
export const addPerformer = createThunk(ADD_PERFORMER);
export const addStudio = createThunk(ADD_STUDIO);
export const clearAddMovie = createAction(CLEAR_ADD_MOVIE);
export const setAddMovieDefault = createAction(SET_ADD_MOVIE_DEFAULT);
export const setAddPerformerDefault = createAction(SET_ADD_PERFORMER_DEFAULT);
export const setAddStudioDefault = createAction(SET_ADD_STUDIO_DEFAULT);
export const setStudiosWithStatus = createAction(SET_STUDIOS_WITH_STATUS);

export const setAddMovieValue = createAction(SET_ADD_MOVIE_VALUE, (payload) => {
return {
Expand All @@ -106,67 +96,12 @@ export const setAddPerformerValue = createAction(
};
}
);
export const setAddStudioValue = createAction(
SET_ADD_STUDIO_VALUE,
(payload) => {
return {
section,
...payload,
};
}
);

//
// Action Handlers

export const actionHandlers = handleThunks({
[LOOKUP_STUDIO]: function (getState, payload, dispatch) {
dispatch(set({ section, isFetching: true }));

if (abortCurrentRequest) {
abortCurrentRequest();
}

const { request, abortRequest } = createAjaxRequest({
url: '/lookup/studio',
data: {
term: payload.term,
},
});

abortCurrentRequest = abortRequest;

request.done((data) => {
data = data.map((movie) => ({
...movie,
internalId: movie.id,
id: movie.foreignId,
}));

dispatch(
batchActions([
update({ section, data }),

set({
section,
isFetching: false,
isPopulated: true,
error: null,
}),
])
);
});

request.fail((xhr) => {
dispatch(
set({
section,
isFetching: false,
isPopulated: false,
error: xhr.aborted ? null : xhr,
})
);
});
},

[LOOKUP_PERFORMER]: function (getState, payload, dispatch) {
dispatch(set({ section, isFetching: true }));
Expand Down Expand Up @@ -270,55 +205,7 @@ export const actionHandlers = handleThunks({
});
},

[ADD_STUDIO]: function (getState, payload, dispatch) {
dispatch(set({ section, isAdding: true }));

const foreignId = payload.foreignId;
const items = getState().addMovie.items;
const itemToAdd = _.find(items, { foreignId });
const newStudio = getNewStudio(_.cloneDeep(itemToAdd.studio), payload);
newStudio.id = 0;

const promise = createAjaxRequest({
url: '/studio',
method: 'POST',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify(newStudio),
}).request;

promise.done((data) => {
const updatedItem = _.cloneDeep(data);
updatedItem.internalId = updatedItem.id;
updatedItem.id = updatedItem.foreignId;
delete updatedItem.images;

const actions = [
updateItem({ section: 'studios', ...data }),
updateItem({ section: 'addMovie', ...updatedItem }),

set({
section,
isAdding: false,
isAdded: true,
addError: null,
}),
];

dispatch(batchActions(actions));
});

promise.fail((xhr) => {
dispatch(
set({
section,
isAdding: false,
isAdded: false,
addError: xhr,
})
);
});
},
});

//
Expand All @@ -328,7 +215,6 @@ export const reducers = createHandleActions(
{
[SET_ADD_MOVIE_VALUE]: createSetSettingValueReducer(section),
[SET_ADD_PERFORMER_VALUE]: createSetSettingValueReducer(section),
[SET_ADD_STUDIO_VALUE]: createSetSettingValueReducer(section),

[SET_ADD_MOVIE_DEFAULT]: function (state, { payload }) {
const newState = getSectionState(state, section);
Expand Down Expand Up @@ -361,12 +247,6 @@ export const reducers = createHandleActions(
return updateSectionState(state, section, newState);
},

[SET_STUDIOS_WITH_STATUS]: function (state, { payload }) {
const newState = getSectionState(state, section);
newState.studiosWithStatus = payload;
return updateSectionState(state, section, newState);
},

[CLEAR_ADD_MOVIE]: function (state) {
const {
movieDefaults,
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/Studio/AddNewStudio/AddNewStudio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { icons, kinds } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import AddNewStudioSearchResult from './AddNewStudioSearchResult';
import useAddNewStudio from './useAddNewStudio';
Expand Down Expand Up @@ -84,7 +83,9 @@ function AddNewStudio(props: AddNewStudioProps) {
<div className={styles.helpText}>
{translate('FailedLoadingSearchResults')}
</div>
<Alert kind={kinds.WARNING}>{getErrorMessage(error)}</Alert>
<Alert kind={kinds.WARNING}>
{error?.statusBody?.message ?? error?.message ?? ''}
</Alert>
<div>
<Link to="https://wiki.servarr.com/whisparr/troubleshooting#invalid-response-received-from-tmdb">
{translate('WhySearchesCouldBeFailing')}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/Studio/AddNewStudio/AddNewStudioModal.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { Image } from 'Studio/Studio';
import Studio, { Image } from 'Studio/Studio';
import AddNewStudioModalContent from './AddNewStudioModalContent';

interface AddNewStudioModalProps {
isOpen: boolean;
onModalClose: () => void;
studio: Studio;
foreignId: string;
title: string;
images: Image[];
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/Studio/AddNewStudio/AddNewStudioModalContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,23 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import { Image } from 'Studio/Studio';
import Studio, { Image } from 'Studio/Studio';
import StudioLogo from 'Studio/StudioLogo';
import { EnhancedSelectInputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import { useAddNewStudioModalContent } from './useAddNewStudio';
import styles from './AddNewStudioModalContent.css';

interface AddNewStudioModalContentProps {
studio: Studio;
foreignId: string;
title: string;
images: Image[];
onModalClose: () => void;
}

function AddNewStudioModalContent(props: AddNewStudioModalContentProps) {
const { foreignId, title, images, onModalClose } = props;
const { title, images, onModalClose, studio } = props;

const {
isAdding,
Expand All @@ -35,7 +36,7 @@ function AddNewStudioModalContent(props: AddNewStudioModalContentProps) {
settings,
onInputChange,
onAddStudioPress,
} = useAddNewStudioModalContent(foreignId);
} = useAddNewStudioModalContent(studio);

const onQualityProfileIdChange = React.useCallback(
({ value }: EnhancedSelectInputChanged<string | number>) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ function AddNewStudioSearchResult({

<AddNewStudioModal
isOpen={isNewAddStudioModalOpen && !isExistingStudio}
studio={studio}
foreignId={foreignId}
title={title}
images={images}
Expand Down
Loading
Loading