mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-09-03 05:10:00 +03:00
feat(deezer): Add family account monitoring #344
This commit is contained in:
@@ -91,6 +91,47 @@ import DeezerDeprecatedConfig from '!!raw-loader!@site/../config/deezer-deprecat
|
||||
|
||||
This option comes with some trade-offs: MS will aggressively detect repeated tracks within a window of time that should eliminate all duplicates. However, this will also prevent *intentionally* repeated tracks from being scrobbled. See [this thread](https://github.com/FoxxMD/multi-scrobbler/pull/296#issuecomment-2970417070) for more information on how this works.
|
||||
|
||||
### Family account
|
||||
|
||||
Multi-scrobbler can monitor listening history for accounts linked to a [Deezer Family Account](https://www.deezer.com/en/offers/family).
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Instructions</summary>
|
||||
|
||||
Start multi-scrobbler using the [ARL](#retrieve-arl) for the **main account**.
|
||||
|
||||
In the logs look for **Linked Accounts** associated with the Deezer Source. It will look something like this:
|
||||
|
||||
```
|
||||
VERBOSE: [App] [Sources] [Deezer - MyDeezer] Linked Accounts:
|
||||
Name: Joe | ID: 166276334 | Private?: Yes
|
||||
Name: Mary | ID: 48475231 | Private?: Yes
|
||||
Name: FoxxMD | ID: 896225281 | Private?: No
|
||||
Name: Cool Guy | ID: 128522478 | Private?: Yes
|
||||
```
|
||||
|
||||
The Account **ID** you want to monitor should be set:
|
||||
|
||||
* ENV Config => `DEEZER_ACCOUNT_ID=896225281`
|
||||
* File Config => `"accountId": "896225281"` in the `data` section of your config
|
||||
|
||||
Restart multi-scrobbler to start monitoring that account.
|
||||
|
||||
:::warning[Restrictions]
|
||||
|
||||
Deezer may not return the correct listening history under these circumstances:
|
||||
|
||||
* The ARL used is **not** for the main account
|
||||
* If you can get an ARL specifically for the linked account (login as the linked account), use that **instead** of family `accountId`
|
||||
* The ARL account is private
|
||||
* The linked account to be monitored ( using `accountId`) is private
|
||||
|
||||
:::
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="official" label="Official API">
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ export interface DeezerInternalData extends CommonSourceData, PollingOptions {
|
||||
* @default "Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0"
|
||||
*/
|
||||
userAgent?: string
|
||||
|
||||
/** The ID (USER_ID) of the linked account to monitor. If not set, monitors the main ARL account */
|
||||
accountId?: string
|
||||
}
|
||||
|
||||
export interface DeezerInternalSourceConfig extends CommonSourceConfig {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { genericSourcePlayMatch } from "../utils/PlayComparisonUtils.js";
|
||||
import { TemporalPlayComparisonOptions } from "../utils/TimeUtils.js";
|
||||
import { findAsync, findIndexAsync } from "../utils/AsyncUtils.js";
|
||||
import { baseFormatPlayObj } from "../utils/PlayTransformUtils.js";
|
||||
import { UpstreamError } from "../common/errors/UpstreamError.js";
|
||||
|
||||
interface DeezerHistoryResponse {
|
||||
errors: []
|
||||
@@ -23,9 +24,50 @@ interface DeezerHistoryResponse {
|
||||
}
|
||||
}
|
||||
|
||||
interface DeezerAccountData {
|
||||
USER_ID: string,
|
||||
/** account name */
|
||||
BLOG_NAME: string,
|
||||
/** https://github.com/FoxxMD/multi-scrobbler/issues/344#issuecomment-3347915743 */
|
||||
EXTRA_FAMILY?: {
|
||||
/** if false then this account is private */
|
||||
IS_LOGGABLE_AS: boolean
|
||||
/** true if private? */
|
||||
IS_DELINKABLE: boolean
|
||||
}
|
||||
}
|
||||
|
||||
interface DeezerAccountResponse {
|
||||
error?: {PERMISSION_ERROR: "No Permission"}
|
||||
results: DeezerAccountData[]
|
||||
}
|
||||
|
||||
interface DeezerUserDataResponse {
|
||||
results: DeezerAuthedUserData & {
|
||||
checkForm: string
|
||||
}
|
||||
}
|
||||
|
||||
interface DeezerAuthedUserData {
|
||||
USER: {
|
||||
USER_ID: string
|
||||
/** account name */
|
||||
BLOG_NAME: string
|
||||
MULTI_ACCOUNT: {
|
||||
/** true if its a sub account */
|
||||
IS_SUB_ACCOUNT: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class DeezerInternalSource extends MemorySource {
|
||||
requiresAuth = true;
|
||||
requiresAuthInteraction = false;
|
||||
isSubAccount: boolean = false;
|
||||
|
||||
authedAccount: DeezerAuthedUserData;
|
||||
|
||||
accounts?: DeezerAccountData[] = []
|
||||
|
||||
csrfToken?: string;
|
||||
|
||||
@@ -119,7 +161,42 @@ export default class DeezerInternalSource extends MemorySource {
|
||||
.query({
|
||||
method: 'deezer.getUserData'
|
||||
})
|
||||
const resp = await this.callApi(req);
|
||||
const resp = (await this.callApi(req)) as DeezerUserDataResponse;
|
||||
this.authedAccount = resp.results;
|
||||
this.logger.verbose(`Authenticated for User ${resp.results.USER.BLOG_NAME}`);
|
||||
const enumerated = await this.enumerateChildAccounts();
|
||||
|
||||
// still a bit unsure about this
|
||||
// https://github.com/FoxxMD/multi-scrobbler/issues/344#issuecomment-3357187332
|
||||
// but it seems like if the authed account is not the *main* account in the family then it is always considered private?
|
||||
if(resp.results.USER.MULTI_ACCOUNT.IS_SUB_ACCOUNT) {
|
||||
this.logger.verbose('This account is a child account, will not enumerate other accounts');
|
||||
this.isSubAccount = true;
|
||||
if(this.config.data.accountId !== undefined) {
|
||||
this.logger.warn('Cannot use accountId when authenticated account is a child account!');
|
||||
}
|
||||
} else {
|
||||
const enumerated = await this.enumerateChildAccounts();
|
||||
if(this.config.data.accountId !== undefined) {
|
||||
if(!enumerated) {
|
||||
this.logger.warn('Unable to verify if account history is available for accountId due to enumeration issue.');
|
||||
} else {
|
||||
const requestedAccount = this.accounts.find(x => x.USER_ID === this.config.data.accountId);
|
||||
if(requestedAccount === undefined) {
|
||||
this.logger.warn(`Could not find a linked account matching ${this.config.data.accountId}. History fetching may fail.`);
|
||||
} else {
|
||||
const authedAccount = this.accounts.find(x => x.USER_ID === this.authedAccount.USER.USER_ID);
|
||||
if(!authedAccount.EXTRA_FAMILY.IS_LOGGABLE_AS && this.config.data.accountId !== this.authedAccount.USER.USER_ID) {
|
||||
this.logger.warn(`Authed Account (${this.authedAccount.USER.USER_ID}) is private and specified accountId is not the same (${this.config.data.accountId}), likely history returned will not be correct.`);
|
||||
} else if(!requestedAccount.EXTRA_FAMILY.IS_LOGGABLE_AS) {
|
||||
this.logger.warn('Account specified by accountId is private, likely returned will not be correct!');
|
||||
}
|
||||
}
|
||||
}
|
||||
this.jar.setCookie(`account_id=${this.config.data.accountId}`, 'https://www.deezer.com');
|
||||
this.logger.verbose(`Set account_id=${this.config.data.accountId}`);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
@@ -167,6 +244,44 @@ export default class DeezerInternalSource extends MemorySource {
|
||||
}
|
||||
}
|
||||
|
||||
enumerateChildAccounts = async (): Promise<boolean> => {
|
||||
try {
|
||||
const resp = await this.getChildAccounts();
|
||||
this.accounts = resp;
|
||||
const accountSummaries: string[] = [];
|
||||
for(const a of this.accounts) {
|
||||
accountSummaries.push(`Name: ${a.BLOG_NAME} | ID: ${a.USER_ID} | Private?: ${a.EXTRA_FAMILY.IS_LOGGABLE_AS ? 'No' : 'Yes'}`);
|
||||
}
|
||||
this.logger.verbose(`Linked Accounts:\n${accountSummaries.join('\n')}`)
|
||||
return true;
|
||||
} catch (e) {
|
||||
if(this.config.data.accountId !== undefined) {
|
||||
this.logger.warn(new Error(`Could not fetch child accounts, likely using 'accountId' will not work!`));
|
||||
} else {
|
||||
this.logger.warn(new Error('Could not enumerate child accounts. You can ignore this if there is no family account or accountId being used.', {cause: e}));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getChildAccounts = async () => {
|
||||
try {
|
||||
const req = this.agent.post('https://www.deezer.com/ajax/gw-light.php')
|
||||
.query({
|
||||
method: 'deezer.getChildAccounts'
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.send({
|
||||
nb: 30,
|
||||
start: 0
|
||||
});
|
||||
const resp = (await this.callApi(req)) as DeezerAccountResponse;
|
||||
return resp.results;
|
||||
} catch (e) {
|
||||
throw new UpstreamError('Unable to get child accounts', {cause: e});
|
||||
}
|
||||
}
|
||||
|
||||
callApi = async (req: request.SuperAgentRequest, retries = 0) => {
|
||||
const {
|
||||
maxRequestRetries = 1,
|
||||
|
||||
@@ -376,6 +376,7 @@ export default class ScrobbleSources {
|
||||
redirectUri: process.env.DEEZER_REDIRECT_URI,
|
||||
accessToken: process.env.DEEZER_ACCESS_TOKEN,
|
||||
arl: process.env.DEEZER_ARL,
|
||||
accountId: process.env.DEEZER_ACCOUNT_ID
|
||||
};
|
||||
if (!Object.values(d).every(x => x === undefined)) {
|
||||
configs.push({
|
||||
|
||||
Reference in New Issue
Block a user