ramda#propOr TypeScript Examples

The following examples show how to use ramda#propOr. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: qr.ts    From back-home-safe with GNU General Public License v3.0 6 votes vote down vote up
getVenueName = (
  decodedJson: DecodedJSON | Location | null,
  language: languageType
) => {
  if (!decodedJson) return "";

  const trimmedZhName = trim(propOr("", "nameZh", decodedJson));
  const trimmedEnName = trim(propOr("", "nameEn", decodedJson));
  const venueId = trim(propOr("", "venueId", decodedJson));

  const chineseName = !isEmpty(trimmedZhName)
    ? trimmedZhName
    : !isEmpty(trimmedEnName)
    ? trimmedEnName
    : // used for taxi license
      venueId;

  const englishName = !isEmpty(trimmedEnName)
    ? trimmedEnName
    : !isEmpty(trimmedZhName)
    ? trimmedZhName
    : // used for taxi license
      venueId;

  return language === languageType.EN ? englishName : chineseName;
}
Example #2
Source File: user.decorator.ts    From radiopanel with GNU General Public License v3.0 6 votes vote down vote up
User = createParamDecorator(// tslint:disable-line variable-name
	(path: string[] = [], context: ExecutionContext): unknown => {
		const request = context.switchToHttp().getRequest() as IncomingMessage;

		const jwtResult = jwt.decode(request.headers.authorization.replace('Bearer ', '')) as any;

		return propOr(null, 'user')(jwtResult);
	}
)
Example #3
Source File: permission.service.ts    From radiopanel with GNU General Public License v3.0 6 votes vote down vote up
private async handleBasicAuth(authToken: string, permissions: string[]): Promise<boolean> {
		const redisClient = this.redisService.getClient();

		const buff = Buffer.from(authToken.replace('Basic ', ''), 'base64');
		const key = buff.toString('ascii').split(':')[0];

		const apiKey = await this.getApiKeyData(key);

		if (!apiKey) {
			throw new ForbiddenException(`Passed api key could not be found`)
		}

		redisClient.multi()
			.incr(`API_KEY_USAGE:${apiKey.uuid}:${new Date().getMinutes()}`)
			.expire(`API_KEY_USAGE:${apiKey.uuid}:${new Date().getMinutes()}`, 59)
			.exec();

		const availablePermissions = (propOr([], 'permissions')(apiKey) as any[]).map((permission) => permission.permission);
		return !!permissions.every(permission => availablePermissions.indexOf(permission) > -1);
	}
Example #4
Source File: user.service.ts    From radiopanel with GNU General Public License v3.0 6 votes vote down vote up
public async find(page = 1, pagesize = 200): Promise<Paginated<any>> {
		const query = this.userRepository.createQueryBuilder('User')
			.leftJoin('User.roles', 'Roles')
			.leftJoin('User._userMeta', 'UserMeta')
			.leftJoin('User.authenticationMethod', 'AuthenticationMethod')
			.select(['User', 'Roles', 'UserMeta', 'AuthenticationMethod.name']);

		const embedded = await query
			.skip((page - 1) * pagesize)
			.take(pagesize)
			.getMany();

		return {
			_embedded: embedded
				.map((user) => ({
					...omit(['_userMeta'])(user),
					...(user._userMeta.find((x) => x.key === 'customData') && { customData: propOr(null, 'value')(user._userMeta.find((x) => x.key === 'customData')) } )
				}))
				.sort((a, b) => {
					return Math.max(...b.roles.map((r) => r.weight)) - Math.max(...a.roles.map((r) => r.weight));
				}),
			_page: {
				totalEntities: await query.getCount(),
				currentPage: page,
				itemsPerPage: pagesize,
			},
		};
	}
Example #5
Source File: azuraHelper.ts    From radiopanel with GNU General Public License v3.0 6 votes vote down vote up
fetchStatusFile = async (tenant: Tenant): Promise<{ streamUrl: string, streamType: string }> => {
	const stationInfo = await got.get<any>(`${tenant.settings?.azuraCastBaseUrl}/api/station/${tenant.settings?.azuraCastStationId}`, {
		resolveBodyOnly: true,
		responseType: 'json'
	});

	const streamType = (stationInfo?.frontend || 'icecast').replace(/[0-9]/g, '');
	const urlSegments = stationInfo?.listen_url.split('/');
	urlSegments.pop();
	const streamUrl = urlSegments.join('/') + '/' + propOr(statusSuffixes.icecast, streamType)(statusSuffixes);

	return {
		streamUrl,
		streamType,
	}
}
Example #6
Source File: note.component.ts    From radiopanel with GNU General Public License v3.0 6 votes vote down vote up
public ngOnInit(): void {
		this.control = new FormControl(propOr(null, 'data')(this.configuration));

		this.control.valueChanges
			.pipe(
				takeUntil(this.componentDestroyed$),
				debounceTime(1000)
			)
			.subscribe((value) => {
				this.applyConfiguration.emit(value);
			});
	}
Example #7
Source File: permission-overwriter.component.ts    From radiopanel with GNU General Public License v3.0 6 votes vote down vote up
public ngOnInit() {
		this.permissions$ = this.permissionQuery.results$
			.pipe(
				tap((permissions) => {
					if (!permissions) {
						return;
					}

					this.categories = permissions;
					const flatPermissions = (permissions || []).reduce((acc, category) => ({
						...acc,
						...category.groups.reduce((groupAcc, group) => ({
							...groupAcc,
							...group.permissions
								.reduce((permAcc, x) => {
									const matchedPermission = this.enabledPermissions
										.find((permission) => permission.permission === x.value);
									return { ...permAcc, [x.value]: [propOr('inherit', 'permissionType')(matchedPermission)] };
								}, {})
						}), {})
					}), {});

					this.control = this.formBuilder.group(flatPermissions);

					this.control.valueChanges.pipe(
						takeUntil(this.componentDestroyed$),
					).subscribe((value) => {
						this.propagateChange(value);
					});
				})
			);

		this.features$ = this.sessionQuery.features$;

		this.permissionService.fetch().pipe(first()).subscribe();
	}
Example #8
Source File: permission-selector.component.ts    From radiopanel with GNU General Public License v3.0 6 votes vote down vote up
public ngOnInit() {
		this.permissions$ = this.permissionQuery.results$
			.pipe(
				tap((permissions) => {
					this.categories = permissions;
					const flatPermissions = (permissions || []).reduce((acc, category) => ({
						...acc,
						...category.groups.reduce((groupAcc, group) => ({
							...groupAcc,
							...group.permissions
								.reduce((permAcc, x) => ({ ...permAcc, [x.value]: [propOr(false, x.value)(this.enabledPermissions)] }), {})
						}), {})
					}), {});

					this.control = this.formBuilder.group(flatPermissions);

					this.control.valueChanges.pipe(
						takeUntil(this.componentDestroyed$),
					).subscribe((value) => {
						this.propagateChange(value);
					});
				})
			);

		this.features$ = this.sessionQuery.features$;

		this.permissionService.fetch().pipe(first()).subscribe();
	}
Example #9
Source File: overrides.ts    From the-fake-backend with ISC License 6 votes vote down vote up
/**
   * Get routes with overrides.
   *
   * @return An array containing all the routes with overrides
   */
  getAll() {
    return this.routeManager
      .getAll()
      .filter(
        pipe(propOr([], 'methods'), filterOverridableMethods, isNotEmpty)
      );
  }
Example #10
Source File: index.tsx    From back-home-safe with GNU General Public License v3.0 5 votes vote down vote up
LeaveModal = ({ id, visible, onDiscard, onFinish }: Props) => {
  const { currentTime } = useTime();
  const { getTravelRecord } = useTravelRecord();
  const [isTimePickModalOpen, setIsTimePickModalOpen] = useState(false);
  const { language } = useI18n();

  const travelRecord = useMemo(
    () => getTravelRecord(id),
    [id, getTravelRecord]
  );

  const place = useMemo(
    () => (travelRecord ? getVenueName(travelRecord, language) : ""),
    [travelRecord, language]
  );

  const handleLeaveNow = () => {
    onFinish(currentTime);
  };

  const handleLeaveEarly = (time: Dayjs) => {
    onFinish(time);
  };

  const venueType = propOr<locationType, typeof travelRecord, locationType>(
    locationType.PLACE,
    "type",
    travelRecord
  );

  return !travelRecord ? (
    <></>
  ) : (
    <>
      <Prompt
        isModalOpen={visible && !isTimePickModalOpen}
        onCancel={() => {
          setIsTimePickModalOpen(false);
          onDiscard();
        }}
        onLeaveNow={handleLeaveNow}
        onLeaved={() => {
          setIsTimePickModalOpen(true);
        }}
        place={place || ""}
        date={dayjs(travelRecord.inTime)}
        outTime={travelRecord.outTime}
        venueType={venueType}
      />
      <TimePickModal
        isModalOpen={visible && isTimePickModalOpen}
        onCancel={() => {
          setIsTimePickModalOpen(false);
          onDiscard();
        }}
        onConfirm={handleLeaveEarly}
        date={dayjs(travelRecord.inTime)}
      />
    </>
  );
}
Example #11
Source File: ConfirmPage.tsx    From back-home-safe with GNU General Public License v3.0 5 votes vote down vote up
ConfirmPage = ({
  travelRecord,
  readOnly = false,
  confirmPageIcon,
  autoLeave = true,
  setAutoLeave,
  autoLeaveHour = 4,
  handleChangeAutoLeaveHour,
  handleLeave,
}: Props) => {
  const { t } = useTranslation("confirm");
  const { language } = useI18n();

  const date = useMemo(() => dayjs(travelRecord.inTime), [travelRecord]);

  const place = useMemo(
    () => getVenueName(travelRecord, language),
    [travelRecord, language]
  );

  const venueType = propOr(locationType.PLACE, "type", travelRecord);

  return (
    <>
      <PageWrapper>
        <Header>
          {confirmPageIcon && <Logo src={confirmPageIcon} />}
          {readOnly ? (
            <Cross src={cross} />
          ) : (
            <Link to="/">
              <Cross src={cross} />
            </Link>
          )}
        </Header>
        <MessageWrapper>
          {venueType === locationType.TAXI ? (
            <>
              <Msg>{t("message.you_have_entered_taxi")}</Msg>
              <License>{t("message.res_mark")}:</License>
            </>
          ) : (
            <Msg>{t("message.you_have_entered_venue")}</Msg>
          )}
          <PlaceWrapper>
            <Place value={place || ""} readOnly />
          </PlaceWrapper>
          <Time>{date.format("YYYY-MM-DD HH:mm")}</Time>
        </MessageWrapper>
        <TickWrapper>
          <TickWrapperInner>
            <Tick src={tick} />
          </TickWrapperInner>
        </TickWrapper>
        <ActionGroup>
          <ConfirmButton shadowed onClick={handleLeave}>
            {venueType === locationType.TAXI
              ? t("button.get_off")
              : t("button.leave")}
          </ConfirmButton>
          <LeaveMessage>{t("message.remember_to_leave")}</LeaveMessage>
          <AutoLeave>
            <CheckBoxWrapper>
              <CheckBox
                checked={autoLeave}
                onChange={setAutoLeave}
                readOnly={isNil(setAutoLeave)}
              />
              {t("form.auto_leave_after_x_hour", { hour: autoLeaveHour })}
            </CheckBoxWrapper>
            <Change onClick={handleChangeAutoLeaveHour}>
              {t("global:button.change")}
            </Change>
          </AutoLeave>
        </ActionGroup>
      </PageWrapper>
    </>
  );
}
Example #12
Source File: index.tsx    From back-home-safe with GNU General Public License v3.0 5 votes vote down vote up
QRReader = () => {
  const { t } = useTranslation("qr_reader");
  const [qrResult, setQrResult] = useState<string | null>(null);
  const browserHistory = useHistory();
  const { createTravelRecord } = useTravelRecord();
  const { language } = useI18n();

  const handleScan = ({ data }: QRCode) => {
    if (!data || isEmpty(data)) return;
    const decodedJson = qrDecode(data);
    if (!decodedJson) return;

    setQrResult(data);
  };

  useEffect(() => {
    if (!qrResult) return;
    const decodedJson = qrDecode(qrResult);
    if (!decodedJson || !getVenueName(decodedJson, language)) return;
    const trimmedZhName = trim(propOr("", "nameZh", decodedJson));
    const trimmedEnName = trim(propOr("", "nameEn", decodedJson));
    const id = uuid();
    const now = dayjs();
    const record = {
      id: uuid(),
      venueId: decodedJson.venueId,
      nameZh: trimmedZhName,
      nameEn: trimmedEnName,
      type: locationType.PLACE,
      inputType: travelRecordInputType.SCAN,
      inTime: now.toISOString(),
      outTime: now.add(4, "hour").toISOString(),
      originalData: decodedJson,
    };
    createTravelRecord(record);

    browserHistory.push({ pathname: `/confirm/${id}`, state: record });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [qrResult, browserHistory]);

  return (
    <PageWrapper>
      <Header backPath="/" name={t("name")} />
      <Message>{t("message.scan_qr_code")}</Message>
      <VideoContainer>
        <Overlay />
        <QRCodeReader onDecode={handleScan} />
      </VideoContainer>
    </PageWrapper>
  );
}
Example #13
Source File: serviceResponseMapper.ts    From radiopanel with GNU General Public License v3.0 5 votes vote down vote up
manualSearchMapper = (service: "deezer" | "spotify", response: any): Partial<Song>[] => {
	if (service === "spotify") {
		const tracks = pathOr<any[]>(null, ['tracks', 'items'])(response);

		return tracks.map(track => ({
			title: prop('name')(track),
			artist: path<string>(['artists', 0, 'name'])(track),
			album: pathOr('Unknown Album', ['album', 'name'])(track),
			graphic: {
				large: pathOr(null, ['album', 'images', 0, 'url'])(track),
				medium: pathOr(null, ['album', 'images', 1, 'url'])(track),
				small: pathOr(null, ['album', 'images', 2, 'url'])(track),
			},
			extraInfo: {
				album: pick(['album_type', 'external_urls', 'href', 'id', 'release_date', 'release_date_precision', 'total_tracks', 'uri'])(propOr({}, 'album')(track)),
				artist: pick(['id', 'name', 'uri', 'href'])(pathOr({}, ['artists', 0])(track)),
				track: pick(['href', 'external_urls', 'uri', 'explicit', 'id'])(track)
			},
			previewUrl: pathOr(null, ['preview_url'])(track),
			releaseDate: pathOr(new Date(), ['album', 'release_date'])(track),
			durationMs: pathOr(1000, ['duration_ms'])(track),
		}));
	}

	if (service === "deezer") {
		const tracks = pathOr<any[]>(null, ['data'])(response);

		return tracks.map(track => ({
			title: prop('title')(track),
			artist: path<string>(['artist', 'name'])(track),
			album: pathOr('Unknown Album', ['album', 'title'])(track),
			graphic: {
				large: pathOr(null, ['album', 'cover_xl'])(track),
				medium: pathOr(null, ['album', 'cover_big'])(track),
				small: pathOr(null, ['album', 'cover_medium'])(track),
			},
			previewUrl: pathOr(null, ['preview'])(track),
			releaseDate: new Date(),
			durationMs: pathOr(1000, ['duration'])(track),
		}));
	}

	if (service === "apple-music") {
		const tracks = pathOr<any[]>(null, ['results'])(response);

		return tracks.map(track => ({
			title: prop('trackName')(track),
			artist: path<string>(['artistName'])(track),
			album: pathOr('Unknown Album', ['collectionName'])(track),
			graphic: {
				large: pathOr(null, ['artworkUrl100'])(track)?.replace('100x100bb', '640x640bb'),
				medium: pathOr(null, ['artworkUrl60'])(track)?.replace('60x60bb', '300x300bb'),
				small: pathOr(null, ['artworkUrl30'])(track)?.replace('30x30bb', '64x64bb'),
			},
			extraInfo: {
				albumUrl: pathOr(null, ['collectionViewUrl'])(track),
				trackUrl: pathOr(null, ['trackViewUrl'])(track),
				artistUrl: pathOr(null, ['artistViewUrl'])(track),
				primaryGenreName: pathOr(null, ['primaryGenreName'])(track),
				albumPrice: pathOr(null, ['collectionPrice'])(track),
				trackPrice: pathOr(null, ['trackPrice'])(track),
				currency: pathOr(null, ['currency'])(track),
				country: pathOr(null, ['country'])(track),
			},
			previewUrl: pathOr(null, ['previewUrl'])(track),
			releaseDate: pathOr(null, ['releaseDate'])(track),
			durationMs: pathOr(1000, ['trackTimeMillis'])(track),
		}));
	}
}
Example #14
Source File: dashboard.page.ts    From radiopanel with GNU General Public License v3.0 5 votes vote down vote up
public pushNewDashboard(): void {
		this.dashboardService.save(this.dashboard.map((dashboard, index: any) => ({
			...dashboard,
			configuration: propOr(null, index)(this.configurations)
		}))).pipe(first()).subscribe();
	}
Example #15
Source File: SaiditIngestStore.ts    From notabug with MIT License 4 votes vote down vote up
export function createSaiditIngest(
  webca: LinguaWebcaClient = universe,
  host: string = 'saidit.net'
): LinguaWebcaStore {
  const app = new ExpressLikeStore()

  app.get(
    '/things/:lastSubmissionId/comments/:lastCommentId',
    async (req, res) => {
      const { lastSubmissionId, lastCommentId } = req.params
      const commentIds = idRange(COMMENT_KIND, lastCommentId, COMMENTS_PER_POLL)
      const submissionIds = idRange(
        SUBMISSION_KIND,
        lastSubmissionId,
        SUBMISSIONS_PER_POLL
      )
      const apiRes = await webca.get(
        `http://${host}/api/info.json?id=${[
          ...submissionIds,
          ...commentIds
        ].join(',')}`
      )
      const children = getListingThings(apiRes)

      res.json(children)
    }
  )

  // tslint:disable-next-line: variable-name
  app.post(`/things/:kind/new`, async (_req, res) => {
    // tslint:disable-next-line no-let
    // let previousId = lastIds[SUBMISSION_KIND]

    const config = await webca.get(
      `notabug://notabug.io/me/pages/config:saidit_ingest/yaml`
    )

    const previousSubmissionId =
      '' + (config && config[`newest_ingested_${SUBMISSION_KIND}`]) || '0'

    const previousCommentId =
      '' + (config && config[`newest_ingested_${COMMENT_KIND}`]) || '0'

    const nativeThings = await app.client.get(
      // `/things/${kind}/after/${previousId}`
      `/things/${previousSubmissionId}/comments/${previousCommentId}`
    )

    if (
      !nativeThings.length &&
      parseInt(previousCommentId, ID_BASE) < parseInt('4tvm', ID_BASE)
    ) {
      const lastIdNum =
        parseInt(previousCommentId, ID_BASE) + COMMENTS_PER_POLL - 1
      const lastId = lastIdNum.toString(ID_BASE)
      console.log('comment drought advance')
      await webca.patch(
        `notabug://notabug.io/me/pages/config:saidit_ingest/yaml`,
        {
          [`newest_ingested_${COMMENT_KIND}`]: lastId
        }
      )
    }

    // tslint:disable-next-line: readonly-array
    const things: any[] = []

    for (const item of sortBy<typeof nativeThings[0]>(
      propOr(0, 'created_utc'),
      nativeThings
    )) {
      if (!item) {
        continue
      }

      // tslint:disable-next-line: no-let
      let thing: any
      const nativeId = item.id

      if (item.kind === SUBMISSION_KIND) {
        if (item.author === '[deleted]' || item.selftext === '[removed]') {
          continue
        }

        thing = {
          author: `${item.author}@SaidIt`,
          body: entities.decode(item.selftext || ''),
          kind: 'submission',
          timestamp: item.created_utc * 1000,
          title: entities.decode(item.title),
          topic: `SaidIt.${item.subreddit}`,
          url: item.selftext ? '' : item.url || ''
        }
      } else if (item.kind === COMMENT_KIND) {
        const linkId = (item.link_id || '').split('_').pop()
        const [replyToKind, replyToSaiditId] = (item.parent_id || '').split('_')

        const submissionThingId =
          linkId &&
          (await webca.get(
            `notabug://notabug.io/me/lists/saidit:${SUBMISSION_KIND}/${linkId}`
          ))
        const replyToThingId =
          replyToSaiditId &&
          (await webca.get(
            `notabug://notabug.io/me/lists/saidit:${replyToKind}/${replyToSaiditId}`
          ))

        if (!submissionThingId) {
          // tslint:disable-next-line: no-console
          console.log('skip item', item)
          await webca.patch(
            `notabug://notabug.io/me/pages/config:saidit_ingest/yaml`,
            {
              [`newest_ingested_${item.kind}`]: nativeId
            }
          )
          continue
        }

        thing = {
          author: `${item.author}@SaidIt`,
          body: entities.decode(item.body || ''),
          kind: 'comment',
          opId: submissionThingId,
          replyToId: replyToThingId || submissionThingId,
          timestamp: item.created_utc * 1000,
          topic: `SaidIt.${item.subreddit}`
        }
      }

      if (!thing) {
        console.log('wtf', item)
        continue
      }

      things.push(thing)

      const thingId = await webca.post(
        `notabug://notabug.io/me/submit/${thing.kind}/${thing.topic}`,
        thing
      )

      // tslint:disable-next-line: no-console
      console.log('posted', thing.kind, thingId)

      await webca.put(
        `notabug://notabug.io/me/lists/saidit:${item.kind}/${nativeId}`,
        thingId
      )

      await webca.patch(
        `notabug://notabug.io/me/pages/config:saidit_ingest/yaml`,
        {
          [`newest_ingested_${item.kind}`]: nativeId
        }
      )

      await new Promise(ok => setTimeout(ok, 1500))
    }

    res.json(things)
  })

  return app.request
}
Example #16
Source File: serviceResponseMapper.ts    From radiopanel with GNU General Public License v3.0 4 votes vote down vote up
serviceResponseMapper = (originalTitle: string, service: "deezer" | "spotify", response: any): Partial<Song> => {
	if (service === "spotify") {
		const track = pathOr(null, ['tracks', 'items', 0])(response);

		return {
			uuid: uuid.v4(),
			externalId: md5(originalTitle),
			title: propOr(path([1])(originalTitle.split('-')) || "", 'name')(track),
			artist: pathOr(path<string>([0])(originalTitle.split('-')), ['artists', 0, 'name'])(track),
			album: pathOr('Unknown Album', ['album', 'name'])(track),
			graphic: {
				large: pathOr(null, ['album', 'images', 0, 'url'])(track),
				medium: pathOr(null, ['album', 'images', 1, 'url'])(track),
				small: pathOr(null, ['album', 'images', 2, 'url'])(track),
			},
			previewUrl: pathOr(null, ['preview_url'])(track),
			releaseDate: pathOr(new Date(), ['album', 'release_date'])(track),
			durationMs: pathOr(1000, ['duration_ms'])(track),
			originalTitle,
			extraInfo: {
				album: pick(['album_type', 'external_urls', 'href', 'id', 'release_date', 'release_date_precision', 'total_tracks', 'uri'])(propOr({}, 'album')(track)),
				artist: pick(['id', 'name', 'uri', 'href'])(pathOr({}, ['artists', 0])(track)),
				track: pick(['href', 'external_urls', 'uri', 'explicit', 'id'])(track || {})
			},
			updatedAt: new Date(),
			createdAt: new Date()
		};
	}

	if (service === "deezer") {
		const track = pathOr(null, ['data', 0])(response);

		return {
			uuid: uuid.v4(),
			externalId: md5(originalTitle),
			title: propOr(path([1])(originalTitle.split('-')) || "", 'title')(track),
			artist: pathOr(path<string>([0])(originalTitle.split('-')), ['artist', 'name'])(track),
			album: pathOr('Unknown Album', ['album', 'title'])(track),
			graphic: {
				large: pathOr(null, ['album', 'cover_xl'])(track),
				medium: pathOr(null, ['album', 'cover_big'])(track),
				small: pathOr(null, ['album', 'cover_medium'])(track),
			},
			previewUrl: pathOr(null, ['preview'])(track),
			releaseDate: new Date(),
			durationMs: pathOr(1000, ['duration'])(track),
			originalTitle,
			extraInfo: {},
			updatedAt: new Date(),
			createdAt: new Date()
		};
	}

	if (service === "apple-music") {
		const track = pathOr(null, ['results', 0])(response);

		return {
			uuid: uuid.v4(),
			externalId: md5(originalTitle),
			title: propOr(path([1])(originalTitle.split('-')) || "", 'trackName')(track),
			artist: pathOr(path<string>([0])(originalTitle.split('-')), ['artistName'])(track),
			album: pathOr('Unknown Album', ['collectionName'])(track),
			graphic: {
				large: pathOr(null, ['artworkUrl100'])(track)?.replace('100x100bb', '640x640bb'),
				medium: pathOr(null, ['artworkUrl60'])(track)?.replace('60x60bb', '300x300bb'),
				small: pathOr(null, ['artworkUrl30'])(track)?.replace('30x30bb', '64x64bb'),
			},
			extraInfo: {
				albumUrl: pathOr(null, ['collectionViewUrl'])(track),
				trackUrl: pathOr(null, ['trackViewUrl'])(track),
				artistUrl: pathOr(null, ['artistViewUrl'])(track),
				primaryGenreName: pathOr(null, ['primaryGenreName'])(track),
				albumPrice: pathOr(null, ['collectionPrice'])(track),
				trackPrice: pathOr(null, ['trackPrice'])(track),
				currency: pathOr(null, ['currency'])(track),
				country: pathOr(null, ['country'])(track),
			},
			previewUrl: pathOr(null, ['previewUrl'])(track),
			releaseDate: pathOr(null, ['releaseDate'])(track),
			durationMs: pathOr(1000, ['trackTimeMillis'])(track),
			originalTitle,
			updatedAt: new Date(),
			createdAt: new Date()
		};
	}
}