mirror of
https://github.com/immich-app/immich.git
synced 2026-06-23 07:06:43 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e4cf79263b |
@@ -73,7 +73,6 @@ jobs:
|
||||
needs: pre-job
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
pull-requests: write
|
||||
if: ${{ github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
|
||||
runs-on: mich
|
||||
@@ -143,18 +142,9 @@ jobs:
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
|
||||
IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
|
||||
IS_MAIN_DEPLOYMENT: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
IS_DEPLOYMENT_BUILD: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork) }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
if [[ $IS_DEPLOYMENT_BUILD == 'true' ]]; then
|
||||
export ANDROID_APP_LABEL='Immich Staging'
|
||||
fi
|
||||
|
||||
if [[ $IS_MAIN == 'true' ]]; then
|
||||
if [[ $IS_MAIN_DEPLOYMENT == 'true' ]]; then
|
||||
export ANDROID_APPLICATION_ID=app.immich.main
|
||||
fi
|
||||
flutter build apk --release
|
||||
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
|
||||
else
|
||||
@@ -168,50 +158,6 @@ jobs:
|
||||
name: release-apk-signed
|
||||
path: mobile/build/app/outputs/flutter-apk/*.apk
|
||||
|
||||
- name: Publish Android APK deployment
|
||||
if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork) }}
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
APK_URL: ${{ steps.upload-apk.outputs.artifact-url }}
|
||||
with:
|
||||
script: |
|
||||
const artifactUrl = process.env.APK_URL;
|
||||
const isPullRequest = context.eventName === "pull_request";
|
||||
const pullNumber = context.payload.pull_request?.number;
|
||||
const environment = isPullRequest ? `mobile-android-apk-pr-${pullNumber}` : "mobile-android-apk";
|
||||
const description = isPullRequest
|
||||
? `Signed Android APK for PR #${pullNumber}`
|
||||
: "Latest signed Android APK from main";
|
||||
const ref = isPullRequest ? context.payload.pull_request.head.sha : context.sha;
|
||||
|
||||
if (!artifactUrl) {
|
||||
throw new Error("The Android APK artifact URL was not generated");
|
||||
}
|
||||
|
||||
const runUrl = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
const deployment = await github.rest.repos.createDeployment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref,
|
||||
environment,
|
||||
description,
|
||||
auto_merge: false,
|
||||
required_contexts: [],
|
||||
production_environment: false,
|
||||
transient_environment: isPullRequest,
|
||||
});
|
||||
|
||||
await github.rest.repos.createDeploymentStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
deployment_id: deployment.data.id,
|
||||
state: "success",
|
||||
environment,
|
||||
environment_url: artifactUrl,
|
||||
log_url: runUrl,
|
||||
description: "Signed APK artifact is ready for testing",
|
||||
});
|
||||
|
||||
- name: Comment APK download link on PR
|
||||
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }}
|
||||
uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ DB_DATA_LOCATION=./postgres
|
||||
# TZ=Etc/UTC
|
||||
|
||||
# The Immich version to use. You can pin this to a specific version like "v2.1.0"
|
||||
IMMICH_VERSION=v3
|
||||
IMMICH_VERSION=v2
|
||||
|
||||
# Connection secret for postgres. You should change it to a random password
|
||||
# Please use only the characters `A-Za-z0-9`, without special characters or spaces
|
||||
|
||||
@@ -19,7 +19,7 @@ If this does not work, try running `docker compose up -d --force-recreate`.
|
||||
|
||||
| Variable | Description | Default | Containers |
|
||||
| :----------------- | :------------------------------ | :-----: | :----------------------- |
|
||||
| `IMMICH_VERSION` | Image tags | `v3` | server, machine learning |
|
||||
| `IMMICH_VERSION` | Image tags | `v2` | server, machine learning |
|
||||
| `UPLOAD_LOCATION` | Host path for uploads | | server |
|
||||
| `DB_DATA_LOCATION` | Host path for Postgres database | | database |
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ docker image prune
|
||||
## Versioning Policy
|
||||
|
||||
Immich follows [semantic versioning][semver], which tags releases in the format `<major>.<minor>.<patch>`. We intend for breaking changes to be limited to major version releases.
|
||||
You can configure your Docker image to point to the current major version by using a metatag, such as `:v3`.
|
||||
You can configure your Docker image to point to the current major version by using a metatag, such as `:v2`.
|
||||
|
||||
Currently, we have no plans to backport patches to earlier versions. We encourage all users to run the most recent release of Immich.
|
||||
Switching back to an earlier version, even within the same minor release tag, is not supported.
|
||||
|
||||
@@ -13,16 +13,6 @@ if (keystorePropertiesFile.exists()) {
|
||||
keystorePropertiesFile.withInputStream { keystoreProperties.load(it) }
|
||||
}
|
||||
|
||||
def androidApplicationId = System.getenv("ANDROID_APPLICATION_ID")?.trim()
|
||||
if (!androidApplicationId) {
|
||||
androidApplicationId = "app.alextran.immich"
|
||||
}
|
||||
|
||||
def androidAppLabel = System.getenv("ANDROID_APP_LABEL")?.trim()
|
||||
if (!androidAppLabel) {
|
||||
androidAppLabel = "Immich"
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
@@ -47,12 +37,11 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId androidApplicationId
|
||||
applicationId "app.alextran.immich"
|
||||
minSdk = 26
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode flutter.versionCode
|
||||
versionName flutter.versionName
|
||||
manifestPlaceholders = [appLabel: androidAppLabel]
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
|
||||
<application android:label="${appLabel}" android:name=".ImmichApp" android:usesCleartextTraffic="true"
|
||||
<application android:label="Immich" android:name=".ImmichApp" android:usesCleartextTraffic="true"
|
||||
android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true"
|
||||
android:largeHeap="true" android:enableOnBackInvokedCallback="false" android:allowBackup="false"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
|
||||
@@ -289,6 +289,34 @@
|
||||
"required": ["albumIds"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "assetDataWebhook",
|
||||
"title": "Trigger Webhook",
|
||||
"description": "POST asset data to any URL",
|
||||
"types": ["AssetV1"],
|
||||
"hostFunctions": true,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"title": "URL",
|
||||
"description": "Asset data will be POSTed to this URL as a JSON object"
|
||||
},
|
||||
"headerName": {
|
||||
"type": "string",
|
||||
"title": "Header name",
|
||||
"description": "The name of an additional header to include with the request (e.g. authentication)"
|
||||
},
|
||||
"headerValue": {
|
||||
"type": "string",
|
||||
"title": "Header value",
|
||||
"description": "The value of the additional header"
|
||||
}
|
||||
},
|
||||
"required": ["url"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "noop1",
|
||||
"title": "DEV: Nested properties",
|
||||
|
||||
Vendored
+2
@@ -5,6 +5,7 @@ declare module 'extism:host' {
|
||||
createAlbum(ptr: PTR): I64;
|
||||
addAssetsToAlbum(ptr: PTR): I64;
|
||||
addAssetsToAlbums(ptr: PTR): I64;
|
||||
httpRequest(ptr: PTR): I64;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,4 +25,5 @@ declare module 'main' {
|
||||
export function assetTimeline(): I32;
|
||||
export function assetTrash(): I32;
|
||||
export function assetAddToAlbums(): I32;
|
||||
export function assetDataWebhook(): I32;
|
||||
}
|
||||
|
||||
@@ -181,3 +181,25 @@ export const assetAddToAlbums = () => {
|
||||
return {};
|
||||
});
|
||||
};
|
||||
|
||||
export const assetDataWebhook = () => {
|
||||
return wrapper<WorkflowType.AssetV1, { url: string; headerName?: string; headerValue?: string }>(
|
||||
({ config, data, functions }) => {
|
||||
let headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (config.headerName && config.headerValue) {
|
||||
headers[config.headerName] = config.headerValue;
|
||||
}
|
||||
|
||||
functions.httpRequest(config.url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data.asset),
|
||||
headers,
|
||||
});
|
||||
|
||||
return {};
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ declare module 'extism:host' {
|
||||
createAlbum(ptr: PTR): I64;
|
||||
addAssetsToAlbum(ptr: PTR): I64;
|
||||
addAssetsToAlbums(ptr: PTR): I64;
|
||||
httpRequest(ptr: PTR): I64;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +34,11 @@ type HostFunctionResult<T> =
|
||||
|
||||
type QueryParams<T extends (...args: any) => any> = Parameters<T>[0];
|
||||
type AlbumSearchDto = QueryParams<typeof getAllAlbums>;
|
||||
type HttpRequestOptions = {
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
};
|
||||
|
||||
export const hostFunctions = (authToken: string) => {
|
||||
const host = Host.getFunctions();
|
||||
@@ -75,5 +81,11 @@ export const hostFunctions = (authToken: string) => {
|
||||
),
|
||||
addAssetsToAlbums: ({ assetIds, albumIds }: AlbumsToAssets) =>
|
||||
call('addAssetsToAlbums', authToken, [{ albumIds, assetIds }]),
|
||||
httpRequest: (url: string, options?: HttpRequestOptions) =>
|
||||
call<[string, HttpRequestOptions | undefined], string>(
|
||||
'httpRequest',
|
||||
authToken,
|
||||
[url, options],
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
</p>
|
||||
|
||||
> [!WARNING]
|
||||
> ⚠️ Değerli fotoğraflarınız ve videolarınız için daima [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) yedekleme planını uygulayın!
|
||||
>
|
||||
> ⚠️ Always follow [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) backup plan for your precious photos and videos!
|
||||
>
|
||||
|
||||
> [!NOTE]
|
||||
> Kurulum dahil olmak üzere resmi belgeleri https://immich.app/ adresinde bulabilirsiniz.
|
||||
|
||||
@@ -129,7 +129,6 @@ from
|
||||
and "integrity_report"."type" = $1
|
||||
where
|
||||
"asset"."deletedAt" is null
|
||||
and "asset"."isExternal" = false
|
||||
and "integrity_report"."createdAt" >= $2
|
||||
and "integrity_report"."createdAt" <= $3
|
||||
order by
|
||||
|
||||
@@ -177,7 +177,6 @@ export class IntegrityRepository {
|
||||
'asset.id as assetId',
|
||||
'integrity_report.id as reportId',
|
||||
])
|
||||
.where('asset.isExternal', '=', sql.lit(false))
|
||||
.$if(startMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '>=', startMarker!))
|
||||
.$if(endMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '<=', endMarker!))
|
||||
.orderBy('integrity_report.createdAt', 'asc')
|
||||
|
||||
@@ -2939,8 +2939,6 @@ describe(MediaService.name, () => {
|
||||
'7',
|
||||
'-global_quality:v',
|
||||
'23',
|
||||
'-b:v',
|
||||
'6897k',
|
||||
'-maxrate',
|
||||
'10000k',
|
||||
'-bufsize',
|
||||
|
||||
@@ -74,12 +74,26 @@ export class WorkflowExecutionService extends BaseService {
|
||||
const addAssetsToAlbums = this.wrap<[dto: AlbumsAddAssetsDto]>((authDto, args) =>
|
||||
albumService.addAssetsToAlbums(authDto, ...args),
|
||||
);
|
||||
const httpRequest = this.wrap<
|
||||
[
|
||||
url: string,
|
||||
options?: {
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
},
|
||||
]
|
||||
>(async (_, args) => {
|
||||
const res = await fetch(...args);
|
||||
return res.text();
|
||||
});
|
||||
|
||||
const functions = {
|
||||
searchAlbums,
|
||||
createAlbum,
|
||||
addAssetsToAlbum,
|
||||
addAssetsToAlbums,
|
||||
httpRequest,
|
||||
};
|
||||
|
||||
const stubs: typeof functions = {
|
||||
@@ -87,6 +101,7 @@ export class WorkflowExecutionService extends BaseService {
|
||||
createAlbum: dummy,
|
||||
addAssetsToAlbum: dummy,
|
||||
addAssetsToAlbums: dummy,
|
||||
httpRequest: dummy,
|
||||
};
|
||||
|
||||
const plugins = await this.pluginRepository.getForLoad();
|
||||
|
||||
@@ -788,12 +788,6 @@ export class QsvSwDecodeConfig extends BaseHWConfig {
|
||||
const options = [`-${this.useCQP() ? 'q:v' : 'global_quality:v'}`, `${this.config.crf}`];
|
||||
const bitrates = this.getBitrateDistribution();
|
||||
if (bitrates.max > 0) {
|
||||
// Workaround for https://github.com/immich-app/immich/issues/29220, to be revisited
|
||||
// QSV seems to ignore -maxrate without -b:v
|
||||
// -b:v alongside global_quality uses QVBR
|
||||
if (!this.useCQP()) {
|
||||
options.push('-b:v', `${bitrates.target}${bitrates.unit}`);
|
||||
}
|
||||
options.push('-maxrate', `${bitrates.max}${bitrates.unit}`, '-bufsize', `${bitrates.max * 2}${bitrates.unit}`);
|
||||
}
|
||||
return options;
|
||||
|
||||
@@ -686,22 +686,6 @@ describe(IntegrityService.name, () => {
|
||||
nextCursor: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip external library files', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const job = ctx.getMock(JobRepository);
|
||||
job.queue.mockResolvedValue(void 0);
|
||||
|
||||
const { user } = await ctx.newUser();
|
||||
|
||||
await ctx.newAsset({ ownerId: user.id, isExternal: true });
|
||||
|
||||
await sut.handleChecksumFiles({ refreshOnly: false });
|
||||
|
||||
await expect(
|
||||
ctx.get(IntegrityRepository).getIntegrityReport({ limit: 100 }, IntegrityReport.ChecksumFail),
|
||||
).resolves.toEqual({ items: [], nextCursor: undefined });
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleChecksumRefresh', () => {
|
||||
|
||||
@@ -324,18 +324,6 @@
|
||||
shortcut: { key: ' ' },
|
||||
onShortcut: () => (videoPlayer?.paused ? videoPlayer?.play() : videoPlayer?.pause()),
|
||||
},
|
||||
{
|
||||
shortcut: { shift: true, key: 'ArrowLeft' },
|
||||
onShortcut: () =>
|
||||
videoPlayer ? (videoPlayer.currentTime = Math.max(videoPlayer.currentTime - 0.4, 0)) : undefined,
|
||||
},
|
||||
{
|
||||
shortcut: { shift: true, key: 'ArrowRight' },
|
||||
onShortcut: () =>
|
||||
videoPlayer
|
||||
? (videoPlayer.currentTime = Math.min(videoPlayer.currentTime + 0.4, videoPlayer.duration))
|
||||
: undefined,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
|
||||
@@ -24,8 +24,7 @@ class FaceManager {
|
||||
});
|
||||
|
||||
readonly people = $derived.by(() => {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const people = new Map<string, PersonResponseDto>();
|
||||
const people = new SvelteMap<string, PersonResponseDto>();
|
||||
|
||||
for (const face of this.data) {
|
||||
if (face.person) {
|
||||
@@ -33,7 +32,7 @@ class FaceManager {
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(people.values());
|
||||
return people.values();
|
||||
});
|
||||
|
||||
readonly facesByPersonId = $derived.by(() => {
|
||||
|
||||
Reference in New Issue
Block a user