diff --git a/docs/en/program-update.md b/docs/en/program-update.md
index e174eaa5b..16cf42096 100644
--- a/docs/en/program-update.md
+++ b/docs/en/program-update.md
@@ -16,6 +16,20 @@ pageClass: routes
+## App Center
+
+### Release
+
+
+
+::: tip
+
+The parameters can be extracted from the Release page URL: `https://install.appcenter.ms/users/:user/apps/:app/distribution_groups/:distribution_group`
+
+:::
+
+
+
## Apkpure
### Versions
diff --git a/docs/program-update.md b/docs/program-update.md
index eac0cd60c..397c1860c 100644
--- a/docs/program-update.md
+++ b/docs/program-update.md
@@ -32,6 +32,20 @@ pageClass: routes
+## App Center
+
+### Release
+
+
+
+::: tip 提示
+
+参数可从 Release 页的 URL 中提取: `https://install.appcenter.ms/users/:user/apps/:app/distribution_groups/:distribution_group`
+
+:::
+
+
+
## App Store/Mac App Store
### 应用更新
diff --git a/lib/v2/app-center/maintainer.js b/lib/v2/app-center/maintainer.js
new file mode 100644
index 000000000..42dcdbe44
--- /dev/null
+++ b/lib/v2/app-center/maintainer.js
@@ -0,0 +1,3 @@
+module.exports = {
+ '/release/:user/:app/:distribution_group': ['Rongronggg9'],
+};
diff --git a/lib/v2/app-center/radar.js b/lib/v2/app-center/radar.js
new file mode 100644
index 000000000..f05c6d8e4
--- /dev/null
+++ b/lib/v2/app-center/radar.js
@@ -0,0 +1,13 @@
+module.exports = {
+ 'appcenter.ms': {
+ _name: 'App Center',
+ install: [
+ {
+ title: 'App Center Release',
+ docs: 'https://docs.rsshub.app/program-update.html#app-center',
+ source: ['/users/:user/apps/:app/distribution_groups/:distribution_group', '/orgs/:user/apps/:app/distribution_groups/:distribution_group'],
+ target: '/app-center/release/:user/:app/:distribution_group',
+ },
+ ],
+ },
+};
diff --git a/lib/v2/app-center/release.js b/lib/v2/app-center/release.js
new file mode 100644
index 000000000..cdd370313
--- /dev/null
+++ b/lib/v2/app-center/release.js
@@ -0,0 +1,121 @@
+const got = require('@/utils/got');
+const { parseDate } = require('@/utils/parse-date');
+const { art } = require('@/utils/render');
+const path = require('path');
+const MarkdownIt = require('markdown-it');
+
+module.exports = async (ctx) => {
+ const user = ctx.params.user;
+ const app = ctx.params.app;
+ const distribution_group = ctx.params.distribution_group;
+
+ const baseUrl = 'https://install.appcenter.ms/api/v0.1/apps';
+ const apiUrl = `${baseUrl}/${user}/${app}/distribution_groups/${distribution_group}`;
+ const releasesListUrl = `${apiUrl}/public_releases?scope=tester`;
+ // const releaseUrl = `${apiUrl}/releases/${release_id}?is_install_page=true`;
+
+ const response = await got(releasesListUrl);
+
+ let title;
+ const link = `https://install.appcenter.ms/users/${user}/apps/${app}/distribution_groups/${distribution_group}`;
+ let icon;
+
+ let items = response.data.map((item) => ({
+ // item:
+ // {
+ // "id": 504,
+ // "short_version": "8.5.0.0",
+ // "version": "18558",
+ // "origin": "appcenter",
+ // "uploaded_at": "2022-02-02T11:36:06.044Z",
+ // "mandatory_update": false,
+ // "enabled": true,
+ // "is_external_build": false
+ // }
+
+ pubDate: parseDate(item.uploaded_at),
+ link: `${apiUrl}/releases/${item.id}?is_install_page=true`,
+ }));
+
+ // Release info examples:
+ // Android: https://install.appcenter.ms/api/v0.1/apps/rafalense-70ux/plus-release/distribution_groups/public/releases/42?is_install_page=true
+ // iOS: https://install.appcenter.ms/api/v0.1/apps/gameonline/baitomobile/distribution_groups/baito/releases/26?is_install_page=true
+ // Windows: https://install.appcenter.ms/api/v0.1/apps/remitano/remitano-windows/distribution_groups/beta/releases/5?is_install_page=true
+ // macOS: https://install.appcenter.ms/api/v0.1/apps/rdmacios-k2vy/microsoft-remote-desktop-for-mac/distribution_groups/all-users-of-microsoft-remote-desktop-for-mac/releases/635?is_install_page=true
+
+ const md = new MarkdownIt();
+ items = await Promise.all(
+ items.map((item) =>
+ ctx.cache.tryGet(item.link, async () => {
+ const releaseResponse = await got(item.link);
+ const releaseInfo = releaseResponse.data;
+
+ const userName = releaseInfo.owner.display_name;
+ const appOS = releaseInfo.app_os;
+ const shortVersion = releaseInfo.short_version; // will be an empty string for Windows
+ const versionCode = releaseInfo.version;
+ const isExternalBuild = releaseInfo.is_external_build;
+ const isMandatoryUpdate = releaseInfo.mandatory_update;
+ // const isLatest = releaseInfo.is_latest; // this is not representing the latest release, but the latest version of a certain release
+ const sizeInMBytes = (releaseInfo.size / (1024 * 1024)).toFixed(2);
+ const releaseDate = releaseInfo.uploaded_at; // use original text here because it is already an ISO 8601 time
+ const fingerprint = releaseInfo.fingerprint;
+ const minOS = releaseInfo.min_os; // `null` for Windows
+ const androidMinApiLevel = releaseInfo.android_min_api_level; // only for Android
+ const deviceFamily = releaseInfo.device_family; // only for iOS, `null` for others
+ const bundleId = releaseInfo.bundle_identifier; // can be a hash or a package name
+ const releaseNotes = releaseInfo.release_notes; // markdown, can be an empty string
+ const downloadUrl = releaseInfo.download_url;
+ const installUrl = releaseInfo.install_url;
+ const fileExtension = releaseInfo.fileExtension;
+
+ if (!(title && icon)) {
+ const appName = releaseInfo.app_display_name;
+ const distributionGroupId = releaseInfo.distribution_group_id;
+ const distributionGroupName = releaseInfo.distribution_groups.filter((group) => group.id === distributionGroupId)[0].display_name;
+ title = `${appName} (${distributionGroupName}) for ${appOS} by ${userName} - App Center Releases`;
+ icon = releaseInfo.app_icon_url;
+ }
+
+ const version = shortVersion && versionCode ? `${shortVersion} (${versionCode})` : shortVersion || versionCode;
+
+ item.title =
+ (isMandatoryUpdate ? '[Mandatory]' : '') +
+ // + (isLatest ? "[Latest]" : "")
+ (isExternalBuild ? '[External Build]' : '') +
+ `Version ${version}`;
+ item.link = link; // replace the link with the release page
+ item.author = userName;
+ item.description = art(
+ path.join(__dirname, 'templates/description.art'),
+ {
+ releaseDate,
+ sizeInMBytes,
+ minOS,
+ deviceFamily,
+ androidMinApiLevel,
+ bundleId,
+ downloadUrl,
+ installUrl,
+ fingerprint,
+ appOS,
+ fileExtension,
+ releaseNotes: releaseNotes && md.render(releaseNotes),
+ },
+ { minimize: true }
+ );
+ item.guid = fingerprint;
+
+ return item;
+ })
+ )
+ );
+
+ ctx.state.data = {
+ title,
+ link,
+ description: title,
+ image: icon,
+ item: items,
+ };
+};
diff --git a/lib/v2/app-center/router.js b/lib/v2/app-center/router.js
new file mode 100644
index 000000000..d1558874b
--- /dev/null
+++ b/lib/v2/app-center/router.js
@@ -0,0 +1,3 @@
+module.exports = function (router) {
+ router.get('/release/:user/:app/:distribution_group', require('./release'));
+};
diff --git a/lib/v2/app-center/templates/description.art b/lib/v2/app-center/templates/description.art
new file mode 100644
index 000000000..3e7a8d8d3
--- /dev/null
+++ b/lib/v2/app-center/templates/description.art
@@ -0,0 +1,24 @@
+
+Release Date: {{releaseDate}}
+Fingerprint: {{fingerprint}}
+OS: {{appOS}}
+{{if minOS}}Minimum OS Version: {{minOS}}
{{/if}}
+{{if androidMinApiLevel}}Android Minimum API Level: {{androidMinApiLevel}}
{{/if}}
+{{if deviceFamily}}Device Family: {{deviceFamily}}
{{/if}}
+{{if bundleId}}Bundle ID: {{bundleId}}
{{/if}}
+{{if fileExtension}}File Extension: {{fileExtension}}
{{/if}}
+Size: {{sizeInMBytes}} MB
+
+{{if releaseNotes}}
+
+Release Notes:
+
+{{@ releaseNotes }}
+{{/if}}
+
+{{if downloadUrl===installUrl}}
+[ Download ]
+{{else}}
+[ Download | Install ]
+{{/if}}
+