diff --git a/.circleci/config.yml b/.circleci/config.yml index 84ac9135699..8f254dbd3db 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -73,7 +73,6 @@ jobs: - ./node_modules - ./packages/web/node_modules - ./packages/mobile/node_modules - - ./packages/probers/node_modules - ./packages/embed/node_modules - ./packages/harmony/node_modules - ./packages/common/node_modules @@ -472,7 +471,6 @@ workflows: packages/discovery-provider/.* run-discovery-workflow true packages/web/.* run-web-workflow true packages/mobile/.* run-mobile-workflow true - packages/probers/.* run-web-workflow true packages/embed/.* run-embed-workflow true packages/common/.* run-common-workflow true packages/common/.* run-web-workflow true @@ -491,6 +489,7 @@ workflows: packages/libs/.* run-web-workflow true packages/libs/.* run-mobile-workflow true packages/libs/.* run-embed-workflow true + packages/libs/.* run-create-audius-app-workflow true packages/create-audius-app/.* run-create-audius-app-workflow true requires: - generate-config @@ -514,7 +513,6 @@ workflows: .* run-release-workflow false .* run-sdk-workflow true .* run-harmony-workflow true - .* run-probers-workflow true .* run-ddex-stage-workflow true .circleci/.* run-web-workflow true .circleci/.* run-mobile-workflow true @@ -526,6 +524,7 @@ workflows: packages/eslint-config-audius/.* run-mobile-workflow true packages/libs/.* run-web-workflow true packages/libs/.* run-mobile-workflow true + packages/libs/.* run-create-audius-app-workflow true packages/harmony/.* run-web-workflow true packages/harmony/.* run-mobile-workflow true packages/mobile/.* run-mobile-workflow true @@ -570,7 +569,6 @@ workflows: .* run-web-workflow true .* run-mobile-workflow true .* run-harmony-workflow true - .* run-probers-workflow false .* run-protocol-dashboard-workflow true requires: - generate-config diff --git a/.circleci/src/@continue_config.yml b/.circleci/src/@continue_config.yml index ec7f21bd9b7..7212643f7c2 100644 --- a/.circleci/src/@continue_config.yml +++ b/.circleci/src/@continue_config.yml @@ -5,9 +5,9 @@ orbs: slack: circleci/slack@4.12.0 aws-cli: circleci/aws-cli@3.1.5 ruby: circleci/ruby@1.2.0 - cypress: cypress-io/cypress@3.3.1 browser-tools: circleci/browser-tools@1.4.4 newman: postman/newman@1.0.0 + swissknife: roopakv/swissknife@0.69.0 # the default pipeline parameters, which will be updated according to # the results of the path-filtering orb in config.yml @@ -52,9 +52,6 @@ parameters: run-mobile-workflow: type: boolean default: false - run-probers-workflow: - type: boolean - default: false run-embed-workflow: type: boolean default: false @@ -79,16 +76,3 @@ parameters: run-create-audius-app-workflow: type: boolean default: false -# Can enable recurring probers against stage at some point -# workflows: -# version: 2.1 -# hourly: -# triggers: -# - schedule: -# cron: '17 * * * *' -# filters: -# branches: -# only: -# - main -# jobs: -# - probers-test-stage diff --git a/.circleci/src/jobs/@create-audius-app-jobs.yml b/.circleci/src/jobs/@create-audius-app-jobs.yml index e3957bc8ca6..22fc2e44e85 100644 --- a/.circleci/src/jobs/@create-audius-app-jobs.yml +++ b/.circleci/src/jobs/@create-audius-app-jobs.yml @@ -36,3 +36,30 @@ create-audius-app-test: name: 'test' command: | npx turbo run test --filter=create-audius-app + +create-audius-app-test-e2e: + working_directory: ~/audius-protocol + resource_class: medium + docker: + - image: cimg/node:18.17 + steps: + - checkout + - attach_workspace: + at: ./ + - run: + name: 'test-e2e' + command: | + npx playwright install --with-deps + npm run setup-test-env -w create-audius-app + cd packages/create-audius-app/examples/react + npm i + npm run build + cd ../../ + npm run test:e2e + - store_test_results: + path: packages/create-audius-app/report.xml + when: always + - store_artifacts: + path: packages/create-audius-app/playwright-report + - store_artifacts: + path: packages/create-audius-app/blob-report diff --git a/.circleci/src/jobs/@probers-jobs.yml b/.circleci/src/jobs/@probers-jobs.yml deleted file mode 100644 index 860af825fc0..00000000000 --- a/.circleci/src/jobs/@probers-jobs.yml +++ /dev/null @@ -1,12 +0,0 @@ -probers-test-stage: - working_directory: ~/audius-protocol - docker: - - image: cimg/node:latest-browsers - steps: - - checkout - - run: - name: run prober tests against staging.audius.co - command: | - cd packages/probers - npx cypress install - npm run cypress:run-stage diff --git a/.circleci/src/jobs/@web-jobs.yml b/.circleci/src/jobs/@web-jobs.yml index 5c969cb545f..78fd8ad3438 100644 --- a/.circleci/src/jobs/@web-jobs.yml +++ b/.circleci/src/jobs/@web-jobs.yml @@ -29,7 +29,6 @@ web-init: paths: - node_modules - packages/web/node_modules - - packages/probers/node_modules - packages/harmony/node_modules - packages/harmony/dist - packages/common/node_modules @@ -91,26 +90,52 @@ web-build-ssr-staging: paths: - node_modules/@brillout/vite-plugin-import-build/dist -web-test-staging: +playwright-tests: working_directory: ~/audius-protocol - resource_class: large - executor: cypress/default + resource_class: medium + parallelism: 4 + docker: + - image: cimg/node:18.17 steps: - checkout - attach_workspace: at: ./ - - cypress/install: - install-command: 'npx cypress install' - install-browsers: true - working-directory: ~/audius-protocol/packages/probers - - cypress/run-tests: - cypress-command: 'npx cypress run --browser chrome' - start-command: 'cd ../.. ; npm run web:stage' - working-directory: ~/audius-protocol/packages/probers + - run: npx playwright install --with-deps + - run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; cd packages/web; npx playwright test --shard=${SHARD}/${CIRCLE_NODE_TOTAL} + - store_test_results: + path: packages/web/report.xml + when: always - store_artifacts: - path: /home/circleci/audius-protocol/packages/probers/cypress/videos + path: packages/web/blob-report + +playwright-tests-report: + working_directory: ~/audius-protocol + docker: + - image: cimg/node:18.17 + steps: + - swissknife/wait_for_job: + job-name: playwright-tests + - swissknife/get-job-number: + job-name: playwright-tests + - swissknife/get_job_artifacts: + job-number: SK_JOB_NUM + file-name-pattern: packages/web/blob-report/report-1.zip + save-to-directory: ./blob-report + - swissknife/get_job_artifacts: + job-number: SK_JOB_NUM + file-name-pattern: packages/web/blob-report/report-2.zip + save-to-directory: ./blob-report + - swissknife/get_job_artifacts: + job-number: SK_JOB_NUM + file-name-pattern: packages/web/blob-report/report-3.zip + save-to-directory: ./blob-report + - swissknife/get_job_artifacts: + job-number: SK_JOB_NUM + file-name-pattern: packages/web/blob-report/report-4.zip + save-to-directory: ./blob-report + - run: npx playwright merge-reports --reporter html ./blob-report - store_artifacts: - path: /home/circleci/audius-protocol/packages/probers/cypress/screenshots + path: playwright-report web-test: working_directory: ~/audius-protocol diff --git a/.circleci/src/workflows/create-audius-app.yml b/.circleci/src/workflows/create-audius-app.yml index 8961fbde284..e7c98e04457 100644 --- a/.circleci/src/workflows/create-audius-app.yml +++ b/.circleci/src/workflows/create-audius-app.yml @@ -9,3 +9,10 @@ jobs: - Vercel requires: - create-audius-app-init + + - create-audius-app-test-e2e: + context: + - Vercel + - create-audius-app + requires: + - create-audius-app-init diff --git a/.circleci/src/workflows/web.yml b/.circleci/src/workflows/web.yml index 0073a0962b5..354c537ea71 100644 --- a/.circleci/src/workflows/web.yml +++ b/.circleci/src/workflows/web.yml @@ -52,12 +52,18 @@ jobs: branches: only: /(^release.*)$/ - - web-test-staging: + - playwright-tests: context: - Audius Client - Probers requires: - - web-init + - web-build-staging + + - playwright-tests-report: + context: + - Probers + requires: + - web-build-staging - web-test: context: Audius Client diff --git a/.gitignore b/.gitignore index b434a36e777..4958265d802 100644 --- a/.gitignore +++ b/.gitignore @@ -207,4 +207,7 @@ combined-patch-file.txt packages/identity-service/emailCache # CloudFlare -.wrangler \ No newline at end of file +.wrangler + +# Playwright +test-results \ No newline at end of file diff --git a/README.md b/README.md index 28b5d17bbf7..924cc337837 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,6 @@ For details on operating an Audius service, getting started with the Token and t | [`identity-service`](packages/identity-service) | Stores encrypted auth ciphertexts and handles oauth artifacts | | [`libs`](./packages/libs) | `@audius/sdk` and legacy shared utilities `libs` | | [`mobile`](./packages/mobile) | The Audius reference mobile application | -| [`probers`](./packages/probers) | E2E web tests | | [`solana-programs`](https://github.com/AudiusProject/audius-protocol/tree/main/solana-programs) | The Solana programs for the Audius protocol, encompassing user account, content listing, and content interaction functionality | | [`spl`](./packages/spl) | Handles Solana instructions for the Audius programs | | [`sql-ts`](./packages/sql-ts) | A typescript database client | diff --git a/dev-tools/audius-compose b/dev-tools/audius-compose index f79ce6c43f3..ebc6f7bf763 100755 --- a/dev-tools/audius-compose +++ b/dev-tools/audius-compose @@ -260,8 +260,6 @@ def build( "--profile=block-explorer", "--profile=uptime", "--profile=ddex", - "--profile=ddex-release-by-release", - "--profile=ddex-batched", "build", *args, *services, @@ -522,7 +520,8 @@ def up( if ddex_release_by_release or ddex_batched: generate_ddex_mongo_key(protocol_dir) - profiles = [f"--profile=ddex-{'release-by-release' if ddex_release_by_release else 'batched'}"] + profiles = ["--profile=ddex"] + os.environ["DDEX_CHOREOGRAPHY"] = "ERNReleaseByRelease" if ddex_release_by_release else "ERNBatched" else: AAO_DIR = pathlib.Path( os.getenv("AAO_DIR", protocol_dir / "../anti-abuse-oracle") @@ -668,6 +667,7 @@ def test_run(protocol_dir, service, args): if service.startswith("ddex"): env["COMPOSE_PROFILES"] = "ddex" + env["DDEX_CHOREOGRAPHY"] = "ERNReleaseByRelease" if service.endswith("release") else "ERNBatched" generate_ddex_mongo_key(protocol_dir) result = subprocess.run( @@ -688,12 +688,7 @@ def test_run(protocol_dir, service, args): ) if result.returncode != 0 and service.startswith("ddex"): - containers = ( - ["ddex-crawler-release-by-release", "ddex-parser-release-by-release"] - if service.endswith("release") - else ["ddex-crawler-batched", "ddex-parser-batched"] - ) - for container in containers: + for container in ["ddex-crawler", "ddex-parser"]: logs_result = subprocess.run( f"docker logs {container}", shell=True, diff --git a/dev-tools/compose/docker-compose.ddex.yml b/dev-tools/compose/docker-compose.ddex.yml index de16635406b..a3ec51719de 100644 --- a/dev-tools/compose/docker-compose.ddex.yml +++ b/dev-tools/compose/docker-compose.ddex.yml @@ -14,9 +14,6 @@ services: - DDEX_PORT=9000 - DDEX_MONGODB_URL=mongodb://mongo:mongo@ddex-mongo:27017/ddex?authSource=admin&replicaSet=rs0 env_file: .env - depends_on: - ddex-mongo-init: - condition: service_completed_successfully ports: - "9000:9000" networks: @@ -32,9 +29,6 @@ services: environment: - DDEX_MONGODB_URL=mongodb://mongo:mongo@ddex-mongo:27017/ddex?authSource=admin&replicaSet=rs0 env_file: .env - depends_on: - ddex-mongo-init: - condition: service_completed_successfully entrypoint: ./ingester --service crawler healthcheck: test: ["CMD-SHELL", "pgrep ./ingester || exit 1"] @@ -54,9 +48,6 @@ services: environment: - DDEX_MONGODB_URL=mongodb://mongo:mongo@ddex-mongo:27017/ddex?authSource=admin&replicaSet=rs0 env_file: .env - depends_on: - ddex-mongo-init: - condition: service_completed_successfully entrypoint: ./ingester --service parser healthcheck: test: ["CMD-SHELL", "pgrep ./ingester || exit 1"] @@ -80,9 +71,6 @@ services: environment: - DDEX_MONGODB_URL=mongodb://mongo:mongo@ddex-mongo:27017/ddex?authSource=admin&replicaSet=rs0 env_file: .env - depends_on: - ddex-mongo-init: - condition: service_completed_successfully ports: - "9001:9001" healthcheck: diff --git a/dev-tools/compose/docker-compose.test.yml b/dev-tools/compose/docker-compose.test.yml index 92d9842a5cb..b7c20429b58 100644 --- a/dev-tools/compose/docker-compose.test.yml +++ b/dev-tools/compose/docker-compose.test.yml @@ -442,63 +442,78 @@ services: # ddex - ddex-crawler-release-by-release: + ddex-crawler: extends: file: docker-compose.yml - service: ddex-crawler-release-by-release - container_name: ddex-crawler-release-by-release + service: ddex-crawler + container_name: ddex-crawler logging: *default-logging environment: - AWS_ENDPOINT: 'http://ddex-s3-release-by-release:4566' - DDEX_CHOREOGRAPHY: 'ERNReleaseByRelease' + AWS_ENDPOINT: 'http://ddex-s3:4566' + DDEX_CHOREOGRAPHY: ${DDEX_CHOREOGRAPHY} TEST_MODE: 'true' depends_on: ddex-mongo-init: condition: service_completed_successfully - ddex-s3-release-by-release: + ddex-s3: condition: service_healthy - ddex-parser-release-by-release: + ddex-parser: extends: file: docker-compose.yml - service: ddex-parser-release-by-release - container_name: ddex-parser-release-by-release + service: ddex-parser + container_name: ddex-parser logging: *default-logging environment: - AWS_ENDPOINT: 'http://ddex-s3-release-by-release:4566' - DDEX_CHOREOGRAPHY: 'ERNReleaseByRelease' + AWS_ENDPOINT: 'http://ddex-s3:4566' + DDEX_CHOREOGRAPHY: ${DDEX_CHOREOGRAPHY} depends_on: ddex-mongo-init: condition: service_completed_successfully - ddex-s3-release-by-release: + ddex-s3: condition: service_healthy - ddex-publisher-release-by-release: + ddex-publisher: extends: file: docker-compose.yml - service: ddex-publisher-release-by-release - container_name: ddex-publisher-release-by-release + service: ddex-publisher + container_name: ddex-publisher logging: *default-logging environment: - AWS_ENDPOINT: 'http://ddex-s3-release-by-release:4566' - DDEX_CHOREOGRAPHY: 'ERNReleaseByRelease' + AWS_ENDPOINT: 'http://ddex-s3:4566' + DDEX_CHOREOGRAPHY: ${DDEX_CHOREOGRAPHY} depends_on: ddex-mongo-init: condition: service_completed_successfully - ddex-s3-release-by-release: + ddex-s3: condition: service_healthy - ddex-s3-release-by-release: - container_name: ddex-s3-release-by-release + ddex-s3: + container_name: ddex-s3 image: localstack/localstack:s3-latest ports: - "127.0.0.1:4566:4566" networks: - ddex-network volumes: - - "ddex-s3-release-by-release:/var/lib/localstack" + - "ddex-s3:/var/lib/localstack" - "/var/run/docker.sock:/var/run/docker.sock" + ddex-mongo: + extends: + file: docker-compose.yml + service: ddex-mongo + logging: *default-logging + + ddex-mongo-init: + extends: + file: docker-compose.yml + service: ddex-mongo-init + logging: *default-logging + depends_on: + ddex-mongo: + condition: service_healthy + test-ddex-e2e-release-by-release: container_name: test-ddex-e2e-release-by-release extends: @@ -507,77 +522,20 @@ services: entrypoint: sh -c '[ ! "$$1" = "test" ] && sleep inf || (shift; go test ./e2e_test/... -count 1 -timeout 3m "$$@")' - logging: *default-logging environment: - AWS_ENDPOINT: 'http://ddex-s3-release-by-release:4566' - DDEX_CHOREOGRAPHY: 'ERNReleaseByRelease' + AWS_ENDPOINT: 'http://ddex-s3:4566' + DDEX_CHOREOGRAPHY: "ERNReleaseByRelease" networks: - ddex-network depends_on: - ddex-crawler-release-by-release: + ddex-crawler: condition: service_healthy - ddex-parser-release-by-release: + ddex-parser: condition: service_healthy # ddex-publisher: # condition: service_healthy # Leaving out publisher for now because it takes a long time to build. # We don't actually upload anything to Audius in the e2e test, but having a "dry run" publisher mode could be useful - ddex-crawler-batched: - extends: - file: docker-compose.yml - service: ddex-crawler-batched - container_name: ddex-crawler-batched - logging: *default-logging - environment: - AWS_ENDPOINT: 'http://ddex-s3-batched:4566' - DDEX_CHOREOGRAPHY: 'ERNBatched' - TEST_MODE: 'true' - depends_on: - ddex-mongo-init: - condition: service_completed_successfully - ddex-s3-batched: - condition: service_healthy - - ddex-parser-batched: - extends: - file: docker-compose.yml - service: ddex-parser-batched - container_name: ddex-parser-batched - logging: *default-logging - environment: - AWS_ENDPOINT: 'http://ddex-s3-batched:4566' - DDEX_CHOREOGRAPHY: 'ERNBatched' - depends_on: - ddex-mongo-init: - condition: service_completed_successfully - ddex-s3-batched: - condition: service_healthy - - ddex-publisher-batched: - extends: - file: docker-compose.yml - service: ddex-publisher-batched - container_name: ddex-publisher-batched - logging: *default-logging - environment: - AWS_ENDPOINT: 'http://ddex-s3-batched:4566' - DDEX_CHOREOGRAPHY: 'ERNBatched' - depends_on: - ddex-mongo-init: - condition: service_completed_successfully - ddex-s3-batched: - condition: service_healthy - - ddex-s3-batched: - container_name: ddex-s3-batched - image: localstack/localstack:s3-latest - ports: - - "127.0.0.1:4566:4566" - networks: - - ddex-network - volumes: - - "ddex-s3-batched:/var/lib/localstack" - - "/var/run/docker.sock:/var/run/docker.sock" - test-ddex-e2e-batched: container_name: test-ddex-e2e-batched extends: @@ -586,29 +544,14 @@ services: entrypoint: sh -c '[ ! "$$1" = "test" ] && sleep inf || (shift; go test ./e2e_test/... -count 1 -timeout 3m "$$@")' - logging: *default-logging environment: - AWS_ENDPOINT: 'http://ddex-s3-batched:4566' - DDEX_CHOREOGRAPHY: 'ERNBatched' + AWS_ENDPOINT: 'http://ddex-s3:4566' + DDEX_CHOREOGRAPHY: "ERNBatched" networks: - ddex-network depends_on: - ddex-crawler-batched: - condition: service_healthy - ddex-parser-batched: + ddex-crawler: condition: service_healthy - - ddex-mongo: - extends: - file: docker-compose.yml - service: ddex-mongo - logging: *default-logging - - ddex-mongo-init: - extends: - file: docker-compose.yml - service: ddex-mongo-init - logging: *default-logging - depends_on: - ddex-mongo: + ddex-parser: condition: service_healthy test-ddex-unittests: @@ -631,5 +574,4 @@ volumes: mediorum: legacy_creator_file_storage: ddex-mongo-db: - ddex-s3-release-by-release: - ddex-s3-batched: + ddex-s3: diff --git a/dev-tools/compose/docker-compose.yml b/dev-tools/compose/docker-compose.yml index 2038a13608b..dbb91b500d5 100644 --- a/dev-tools/compose/docker-compose.yml +++ b/dev-tools/compose/docker-compose.yml @@ -191,18 +191,17 @@ services: <<: *common profiles: - ddex - - ddex-release-by-release - - ddex-batched ddex-mongo-init: extends: file: docker-compose.ddex.yml service: ddex-mongo-init <<: *common + depends_on: + ddex-mongo: + condition: service_healthy profiles: - ddex - - ddex-release-by-release - - ddex-batched ddex-ingester: extends: @@ -218,170 +217,88 @@ services: service: ddex-web container_name: ddex-web <<: *common - profiles: - - ddex - - ddex-publisher: - extends: - file: docker-compose.ddex.yml - service: ddex-publisher - container_name: ddex-publisher - <<: *common - profiles: - - ddex - - # DDEX release-by-release (only used locally, not pushed to docker hub) - - ddex-web-release-by-release: - extends: - file: docker-compose.ddex.yml - service: ddex-web - container_name: ddex-web-release-by-release - <<: *common - environment: - AWS_ENDPOINT: 'http://ddex-s3-release-by-release:4566' - DDEX_CHOREOGRAPHY: 'ERNReleaseByRelease' - profiles: - - ddex-release-by-release - - ddex-crawler-release-by-release: - extends: - file: docker-compose.ddex.yml - service: ddex-crawler - container_name: ddex-crawler-release-by-release - <<: *common - environment: - AWS_ENDPOINT: 'http://ddex-s3-release-by-release:4566' - DDEX_CHOREOGRAPHY: 'ERNReleaseByRelease' - depends_on: - ddex-mongo-init: - condition: service_completed_successfully - ddex-s3-release-by-release: - condition: service_healthy - profiles: - - ddex-release-by-release - - ddex-parser-release-by-release: - extends: - file: docker-compose.ddex.yml - service: ddex-parser - container_name: ddex-parser-release-by-release - <<: *common environment: - AWS_ENDPOINT: 'http://ddex-s3-release-by-release:4566' - DDEX_CHOREOGRAPHY: 'ERNReleaseByRelease' + AWS_ENDPOINT: 'http://ingress:4566' + DDEX_CHOREOGRAPHY: ${DDEX_CHOREOGRAPHY} depends_on: ddex-mongo-init: condition: service_completed_successfully - ddex-s3-release-by-release: - condition: service_healthy profiles: - - ddex-release-by-release + - ddex + extra_hosts: + - 'ingress:host-gateway' - ddex-publisher-release-by-release: + ddex-publisher: extends: file: docker-compose.ddex.yml service: ddex-publisher - container_name: ddex-publisher-release-by-release + container_name: ddex-publisher <<: *common environment: - AWS_ENDPOINT: 'http://ddex-s3-release-by-release:4566' - DDEX_CHOREOGRAPHY: 'ERNReleaseByRelease' + AWS_ENDPOINT: 'http://ingress:4566' + DDEX_CHOREOGRAPHY: ${DDEX_CHOREOGRAPHY} depends_on: ddex-mongo-init: condition: service_completed_successfully - ddex-s3-release-by-release: + ddex-s3: condition: service_healthy profiles: - - ddex-release-by-release - - ddex-s3-release-by-release: - extends: - file: docker-compose.ddex.yml - service: ddex-s3 - container_name: ddex-s3-release-by-release - <<: *common - volumes: - - "ddex-s3-release-by-release:/var/lib/localstack" - - "/var/run/docker.sock:/var/run/docker.sock" - profiles: - - ddex-release-by-release - # DDEX batched (only used locally, not pushed to docker hub) - - ddex-web-batched: - extends: - file: docker-compose.ddex.yml - service: ddex-web - container_name: ddex-web-batched - <<: *common - environment: - AWS_ENDPOINT: 'http://ddex-s3-batched:4566' - DDEX_CHOREOGRAPHY: 'ERNBatched' - profiles: - - ddex-batched - - ddex-crawler-batched: + - ddex + extra_hosts: + - 'ingress:host-gateway' + + ddex-crawler: extends: file: docker-compose.ddex.yml service: ddex-crawler - container_name: ddex-crawler-batched + container_name: ddex-crawler <<: *common environment: - AWS_ENDPOINT: 'http://ddex-s3-batched:4566' - DDEX_CHOREOGRAPHY: 'ERNBatched' + AWS_ENDPOINT: 'http://ingress:4566' + DDEX_CHOREOGRAPHY: ${DDEX_CHOREOGRAPHY} + IS_DEV: 'true' # Speed up crawling interval for local dev depends_on: ddex-mongo-init: condition: service_completed_successfully - ddex-s3-batched: + ddex-s3: condition: service_healthy profiles: - - ddex-batched - - ddex-parser-batched: + - ddex + extra_hosts: + - 'ingress:host-gateway' + + ddex-parser: extends: file: docker-compose.ddex.yml service: ddex-parser - container_name: ddex-parser-batched - <<: *common - environment: - AWS_ENDPOINT: 'http://ddex-s3-batched:4566' - DDEX_CHOREOGRAPHY: 'ERNBatched' - depends_on: - ddex-mongo-init: - condition: service_completed_successfully - ddex-s3-batched: - condition: service_healthy - profiles: - - ddex-batched - - ddex-publisher-batched: - extends: - file: docker-compose.ddex.yml - service: ddex-publisher - container_name: ddex-publisher-batched + container_name: ddex-parser <<: *common environment: - AWS_ENDPOINT: 'http://ddex-s3-batched:4566' - DDEX_CHOREOGRAPHY: 'ERNBatched' + AWS_ENDPOINT: 'http://ingress:4566' + DDEX_CHOREOGRAPHY: ${DDEX_CHOREOGRAPHY} depends_on: ddex-mongo-init: condition: service_completed_successfully - ddex-s3-batched: + ddex-s3: condition: service_healthy profiles: - - ddex-batched + - ddex + extra_hosts: + - 'ingress:host-gateway' - ddex-s3-batched: + ddex-s3: extends: file: docker-compose.ddex.yml service: ddex-s3 - container_name: ddex-s3-batched + container_name: ddex-s3 <<: *common + ports: + - "127.0.0.1:4566:4566" volumes: - - "ddex-s3-batched:/var/lib/localstack" + - "ddex-s3:/var/lib/localstack" - "/var/run/docker.sock:/var/run/docker.sock" profiles: - - ddex-batched + - ddex # Storage (content node) @@ -455,5 +372,4 @@ volumes: mediorum: legacy_creator_file_storage: ddex-mongo-db: - ddex-s3-release-by-release: - ddex-s3-batched: + ddex-s3: diff --git a/mediorum/.version.json b/mediorum/.version.json index 10632fad1fd..3cf949cd595 100644 --- a/mediorum/.version.json +++ b/mediorum/.version.json @@ -1,4 +1,4 @@ { - "version": "0.6.64", + "version": "0.6.67", "service": "content-node" } diff --git a/package-lock.json b/package-lock.json index 2252ccabda3..ad3eae603fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { "name": "root", - "version": "1.5.72", + "version": "1.5.73", "lockfileVersion": 3, "requires": true, "cacheBust": 2, "packages": { "": { "name": "root", - "version": "1.5.72", + "version": "1.5.73", "hasInstallScript": true, "workspaces": [ "packages/*", @@ -5192,6 +5192,7 @@ }, "node_modules/@colors/colors": { "version": "1.5.0", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -5282,21 +5283,6 @@ "node": ">=10" } }, - "node_modules/@cypress/xvfb": { - "version": "1.2.4", - "license": "MIT", - "dependencies": { - "debug": "^3.1.0", - "lodash.once": "^4.1.1" - } - }, - "node_modules/@cypress/xvfb/node_modules/debug": { - "version": "3.2.7", - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "dev": true, @@ -14706,6 +14692,21 @@ "version": "2.6.2", "license": "0BSD" }, + "node_modules/@playwright/test": { + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz", + "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==", + "dev": true, + "dependencies": { + "playwright": "1.42.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@pm2/agent": { "version": "2.0.3", "license": "AGPL-3.0", @@ -41530,75 +41531,6 @@ } } }, - "node_modules/@testing-library/cypress": { - "version": "9.0.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.14.6", - "@testing-library/dom": "^8.1.0" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "cypress": "^12.0.0" - } - }, - "node_modules/@testing-library/cypress/node_modules/@testing-library/dom": { - "version": "8.20.1", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@testing-library/cypress/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@testing-library/cypress/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@testing-library/cypress/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@testing-library/dom": { "version": "9.3.3", "dev": true, @@ -43696,6 +43628,7 @@ }, "node_modules/@types/aria-query": { "version": "5.0.3", + "dev": true, "license": "MIT" }, "node_modules/@types/array.prototype.flat": { @@ -44446,6 +44379,17 @@ "resolved": "https://registry.npmjs.org/@types/get-params/-/get-params-0.1.2.tgz", "integrity": "sha512-ujqPyr1UDsOTDngJPV+WFbR0iHT5AfZKlNPMX6XOCnQcMhEqR+r64dVC/nwYCitqjR3DcpWofnOEAInUQmI/eA==" }, + "node_modules/@types/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", + "dev": true, + "dependencies": { + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.8", "license": "MIT", @@ -44691,6 +44635,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, "node_modules/@types/minimist": { "version": "1.2.4", "dev": true, @@ -45219,10 +45169,6 @@ "integrity": "sha512-igYpe5ApGMB7YGk2ZyyvrT1NwLYG7Q+8d78uskiS3qriHQa1fiFesibFTCDbGWhc9teD7RmGSuh9a1rzzXj9zg==", "dev": true }, - "node_modules/@types/sinonjs__fake-timers": { - "version": "8.1.1", - "license": "MIT" - }, "node_modules/@types/sizzle": { "version": "2.3.5", "license": "MIT" @@ -45409,6 +45355,7 @@ }, "node_modules/@types/yauzl": { "version": "2.10.2", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -47426,6 +47373,7 @@ }, "node_modules/aggregate-error": { "version": "3.1.0", + "dev": true, "license": "MIT", "dependencies": { "clean-stack": "^2.0.0", @@ -47539,15 +47487,6 @@ "ajv": "^6.9.1" } }, - "node_modules/ally.js": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "css.escape": "^1.5.0", - "platform": "1.3.3" - } - }, "node_modules/amdefine": { "version": "1.0.1", "license": "BSD-3-Clause OR MIT", @@ -48090,6 +48029,7 @@ }, "node_modules/arch": { "version": "2.2.0", + "dev": true, "funding": [ { "type": "github", @@ -48178,6 +48118,7 @@ }, "node_modules/aria-query": { "version": "5.1.3", + "dev": true, "license": "Apache-2.0", "dependencies": { "deep-equal": "^2.0.5" @@ -48185,6 +48126,7 @@ }, "node_modules/aria-query/node_modules/deep-equal": { "version": "2.2.2", + "dev": true, "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.0", @@ -48536,6 +48478,7 @@ }, "node_modules/astral-regex": { "version": "2.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -50455,10 +50398,6 @@ "node": ">= 0.8.0" } }, - "node_modules/blob-util": { - "version": "2.0.2", - "license": "Apache-2.0" - }, "node_modules/bloomfilter": { "version": "0.0.18", "license": "BSD-3-Clause" @@ -51701,13 +51640,6 @@ "node": ">=8" } }, - "node_modules/cachedir": { - "version": "2.4.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/caching-transform": { "version": "4.0.0", "dev": true, @@ -52211,13 +52143,6 @@ "node": "*" } }, - "node_modules/check-more-types": { - "version": "2.24.0", - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/checkpoint-store": { "version": "1.1.0", "license": "ISC", @@ -52486,6 +52411,7 @@ }, "node_modules/clean-stack": { "version": "2.2.0", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -52564,6 +52490,7 @@ }, "node_modules/cli-table3": { "version": "0.6.3", + "dev": true, "license": "MIT", "dependencies": { "string-width": "^4.2.0" @@ -52620,7 +52547,9 @@ }, "node_modules/cli-truncate": { "version": "2.1.0", + "dev": true, "license": "MIT", + "optional": true, "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" @@ -53012,13 +52941,6 @@ "dev": true, "license": "ISC" }, - "node_modules/common-tags": { - "version": "1.8.2", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/commondir": { "version": "1.0.1", "license": "MIT" @@ -54348,28 +54270,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cypress-file-upload": { - "version": "5.0.8", - "license": "MIT", - "engines": { - "node": ">=8.2.1" - }, - "peerDependencies": { - "cypress": ">3.0.0" - } - }, - "node_modules/cypress-plugin-tab": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "ally.js": "^1.4.1" - } - }, - "node_modules/cypress-wait-until": { - "version": "1.7.2", - "license": "MIT" - }, "node_modules/d": { "version": "1.0.1", "license": "ISC", @@ -56626,6 +56526,7 @@ }, "node_modules/dom-accessibility-api": { "version": "0.5.16", + "dev": true, "license": "MIT" }, "node_modules/dom-align": { @@ -56885,10 +56786,6 @@ "xtend": "^4.0.0" } }, - "node_modules/earcut": { - "version": "2.2.4", - "license": "ISC" - }, "node_modules/eastasianwidth": { "version": "0.2.0", "dev": true, @@ -57208,6 +57105,7 @@ }, "node_modules/enquirer": { "version": "2.4.1", + "dev": true, "license": "MIT", "dependencies": { "ansi-colors": "^4.1.1", @@ -57219,6 +57117,7 @@ }, "node_modules/enquirer/node_modules/ansi-colors": { "version": "4.1.3", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -58576,17 +58475,6 @@ "ms": "^2.1.1" } }, - "node_modules/eslint-plugin-cypress": { - "version": "2.12.1", - "dev": true, - "license": "MIT", - "dependencies": { - "globals": "^11.12.0" - }, - "peerDependencies": { - "eslint": ">= 3.2.1" - } - }, "node_modules/eslint-plugin-es": { "version": "4.1.0", "dev": true, @@ -60487,6 +60375,7 @@ }, "node_modules/execa": { "version": "4.1.0", + "dev": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.0", @@ -60508,6 +60397,7 @@ }, "node_modules/execa/node_modules/cross-spawn": { "version": "7.0.3", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -60520,6 +60410,7 @@ }, "node_modules/execa/node_modules/path-key": { "version": "3.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -60527,6 +60418,7 @@ }, "node_modules/execa/node_modules/shebang-command": { "version": "2.0.0", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -60537,6 +60429,7 @@ }, "node_modules/execa/node_modules/shebang-regex": { "version": "3.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -60553,23 +60446,6 @@ "node": ">=8" } }, - "node_modules/executable": { - "version": "4.1.1", - "license": "MIT", - "dependencies": { - "pify": "^2.2.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/executable/node_modules/pify": { - "version": "2.3.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/exif-parser": { "version": "0.1.12" }, @@ -61407,6 +61283,7 @@ }, "node_modules/extract-zip": { "version": "2.0.1", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", @@ -63187,17 +63064,6 @@ "version": "2.3.0", "license": "MIT" }, - "node_modules/getos": { - "version": "3.2.1", - "license": "MIT", - "dependencies": { - "async": "^3.2.0" - } - }, - "node_modules/getos/node_modules/async": { - "version": "3.2.5", - "license": "MIT" - }, "node_modules/getpass": { "version": "0.1.7", "license": "MIT", @@ -63553,6 +63419,7 @@ }, "node_modules/global-dirs": { "version": "3.0.1", + "dev": true, "license": "MIT", "dependencies": { "ini": "2.0.0" @@ -63566,6 +63433,7 @@ }, "node_modules/global-dirs/node_modules/ini": { "version": "2.0.0", + "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -64813,18 +64681,6 @@ "node": ">= 6" } }, - "node_modules/http-signature": { - "version": "1.3.6", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^2.0.2", - "sshpk": "^1.14.1" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/http-status-codes": { "version": "2.3.0", "license": "MIT" @@ -64874,6 +64730,7 @@ }, "node_modules/human-signals": { "version": "1.1.1", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8.12.0" @@ -65136,6 +64993,7 @@ }, "node_modules/indent-string": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -65564,6 +65422,7 @@ }, "node_modules/is-ci": { "version": "3.0.1", + "dev": true, "license": "MIT", "dependencies": { "ci-info": "^3.2.0" @@ -65809,6 +65668,7 @@ }, "node_modules/is-installed-globally": { "version": "0.4.0", + "dev": true, "license": "MIT", "dependencies": { "global-dirs": "^3.0.0", @@ -65952,6 +65812,7 @@ }, "node_modules/is-path-inside": { "version": "3.0.3", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -66210,10 +66071,6 @@ "version": "2.0.0", "license": "ISC" }, - "node_modules/ismobilejs": { - "version": "0.5.2", - "license": "MIT" - }, "node_modules/isobject": { "version": "3.0.1", "license": "MIT", @@ -71138,35 +70995,6 @@ "semver": "bin/semver" } }, - "node_modules/jsprim": { - "version": "2.0.2", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } - }, - "node_modules/jsprim/node_modules/json-schema": { - "version": "0.4.0", - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, - "node_modules/jsprim/node_modules/verror": { - "version": "1.10.0", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "license": "MIT", @@ -71488,13 +71316,6 @@ "node": ">=0.2.0" } }, - "node_modules/lazy-ass": { - "version": "1.6.0", - "license": "MIT", - "engines": { - "node": "> 0.8" - } - }, "node_modules/lazy-universal-dotenv": { "version": "3.0.1", "dev": true, @@ -71789,59 +71610,6 @@ "version": "4.1.0", "license": "MIT" }, - "node_modules/listr2": { - "version": "3.14.0", - "license": "MIT", - "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.16", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.5.1", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" - }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } - } - }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "7.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/lit": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", @@ -72222,50 +71990,6 @@ "node": ">=8" } }, - "node_modules/log-update": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, "node_modules/logfmt": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/logfmt/-/logfmt-1.4.0.tgz", @@ -72441,6 +72165,7 @@ }, "node_modules/lz-string": { "version": "1.5.0", + "dev": true, "license": "MIT", "bin": { "lz-string": "bin/bin.js" @@ -75034,10 +74759,6 @@ "node": ">=4" } }, - "node_modules/mini-signals": { - "version": "1.2.0", - "license": "MIT" - }, "node_modules/minimalistic-assert": { "version": "1.0.1", "license": "ISC" @@ -80015,10 +79736,6 @@ "node": ">=0.10.0" } }, - "node_modules/ospath": { - "version": "1.2.2", - "license": "MIT" - }, "node_modules/outvariant": { "version": "1.4.0", "dev": true, @@ -80099,6 +79816,7 @@ }, "node_modules/p-map": { "version": "4.0.0", + "dev": true, "license": "MIT", "dependencies": { "aggregate-error": "^3.0.0" @@ -80447,13 +80165,6 @@ "version": "1.0.1", "license": "MIT" }, - "node_modules/parse-uri": { - "version": "1.0.9", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/parse5": { "version": "7.1.2", "dev": true, @@ -81117,28 +80828,6 @@ "pixelmatch": "bin/pixelmatch" } }, - "node_modules/pixi-gl-core": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/pixi.js": { - "version": "4.8.9", - "license": "MIT", - "dependencies": { - "bit-twiddle": "^1.0.2", - "earcut": "^2.1.4", - "eventemitter3": "^2.0.0", - "ismobilejs": "^0.5.1", - "object-assign": "^4.0.1", - "pixi-gl-core": "^1.1.4", - "remove-array-items": "^1.0.0", - "resource-loader": "^2.2.3" - } - }, - "node_modules/pixi.js/node_modules/eventemitter3": { - "version": "2.0.3", - "license": "MIT" - }, "node_modules/pkg-conf": { "version": "3.1.0", "dev": true, @@ -81292,17 +80981,13 @@ "pathe": "^1.1.0" } }, - "node_modules/platform": { - "version": "1.3.3", - "dev": true, - "license": "MIT" - }, "node_modules/playwright": { - "version": "1.39.0", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", + "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.39.0" + "playwright-core": "1.42.1" }, "bin": { "playwright": "cli.js" @@ -81315,9 +81000,10 @@ } }, "node_modules/playwright-core": { - "version": "1.39.0", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", + "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", "dev": true, - "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, @@ -82411,16 +82097,6 @@ "node": ">=6.0.0" } }, - "node_modules/pretty-bytes": { - "version": "5.6.0", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/pretty-error": { "version": "4.0.0", "dev": true, @@ -82432,6 +82108,7 @@ }, "node_modules/pretty-format": { "version": "27.5.1", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1", @@ -82444,6 +82121,7 @@ }, "node_modules/pretty-format/node_modules/react-is": { "version": "17.0.2", + "dev": true, "license": "MIT" }, "node_modules/pretty-hrtime": { @@ -82465,10 +82143,6 @@ "node": ">= 0.6" } }, - "node_modules/probers": { - "resolved": "packages/probers", - "link": true - }, "node_modules/proc-log": { "version": "3.0.0", "dev": true, @@ -87852,10 +87526,6 @@ "node": ">=8" } }, - "node_modules/remove-array-items": { - "version": "1.1.1", - "license": "MIT" - }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "dev": true, @@ -88053,13 +87723,6 @@ "node": ">= 4" } }, - "node_modules/request-progress": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "throttleit": "^1.0.0" - } - }, "node_modules/request-promise-core": { "version": "1.1.4", "dev": true, @@ -88409,14 +88072,6 @@ "node": ">=10" } }, - "node_modules/resource-loader": { - "version": "2.2.4", - "license": "MIT", - "dependencies": { - "mini-signals": "^1.1.1", - "parse-uri": "^1.0.0" - } - }, "node_modules/responselike": { "version": "1.0.2", "license": "MIT", @@ -88481,10 +88136,6 @@ "version": "1.5.3", "license": "MIT" }, - "node_modules/rfdc": { - "version": "1.3.0", - "license": "MIT" - }, "node_modules/right-now": { "version": "1.0.0", "license": "MIT" @@ -91572,7 +91223,9 @@ }, "node_modules/slice-ansi": { "version": "3.0.0", + "dev": true, "license": "MIT", + "optional": true, "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", @@ -91584,7 +91237,9 @@ }, "node_modules/slice-ansi/node_modules/ansi-styles": { "version": "4.3.0", + "dev": true, "license": "MIT", + "optional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -97548,10 +97203,6 @@ "node": ">=10" } }, - "node_modules/throttleit": { - "version": "1.0.0", - "license": "MIT" - }, "node_modules/through": { "version": "2.3.8", "license": "MIT" @@ -98053,6 +97704,7 @@ }, "node_modules/tough-cookie": { "version": "4.1.3", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "psl": "^1.1.33", @@ -98066,6 +97718,7 @@ }, "node_modules/tough-cookie/node_modules/punycode": { "version": "2.3.1", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -98073,6 +97726,7 @@ }, "node_modules/tough-cookie/node_modules/universalify": { "version": "0.2.0", + "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -105675,178 +105329,6 @@ "version": "4.0.4", "license": "MIT" }, - "node_modules/web3-rpc-methods": { - "version": "1.1.3", - "license": "LGPL-3.0", - "dependencies": { - "web3-core": "^4.3.0", - "web3-types": "^1.3.0", - "web3-validator": "^2.0.3" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "node_modules/web3-rpc-methods/node_modules/@types/ws": { - "version": "8.5.3", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/web3-rpc-methods/node_modules/cross-fetch": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.12" - } - }, - "node_modules/web3-rpc-methods/node_modules/node-fetch": { - "version": "2.7.0", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/web3-rpc-methods/node_modules/tr46": { - "version": "0.0.3", - "license": "MIT" - }, - "node_modules/web3-rpc-methods/node_modules/web3-core": { - "version": "4.3.1", - "license": "LGPL-3.0", - "dependencies": { - "web3-errors": "^1.1.4", - "web3-eth-iban": "^4.0.7", - "web3-providers-http": "^4.1.0", - "web3-providers-ws": "^4.0.7", - "web3-types": "^1.3.1", - "web3-utils": "^4.0.7", - "web3-validator": "^2.0.3" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - }, - "optionalDependencies": { - "web3-providers-ipc": "^4.0.7" - } - }, - "node_modules/web3-rpc-methods/node_modules/web3-eth-iban": { - "version": "4.0.7", - "license": "LGPL-3.0", - "dependencies": { - "web3-errors": "^1.1.3", - "web3-types": "^1.3.0", - "web3-utils": "^4.0.7", - "web3-validator": "^2.0.3" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "node_modules/web3-rpc-methods/node_modules/web3-providers-http": { - "version": "4.1.0", - "license": "LGPL-3.0", - "dependencies": { - "cross-fetch": "^4.0.0", - "web3-errors": "^1.1.3", - "web3-types": "^1.3.0", - "web3-utils": "^4.0.7" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "node_modules/web3-rpc-methods/node_modules/web3-providers-ipc": { - "version": "4.0.7", - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "web3-errors": "^1.1.3", - "web3-types": "^1.3.0", - "web3-utils": "^4.0.7" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "node_modules/web3-rpc-methods/node_modules/web3-providers-ws": { - "version": "4.0.7", - "license": "LGPL-3.0", - "dependencies": { - "@types/ws": "8.5.3", - "isomorphic-ws": "^5.0.0", - "web3-errors": "^1.1.3", - "web3-types": "^1.3.0", - "web3-utils": "^4.0.7", - "ws": "^8.8.1" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "node_modules/web3-rpc-methods/node_modules/web3-utils": { - "version": "4.0.7", - "license": "LGPL-3.0", - "dependencies": { - "ethereum-cryptography": "^2.0.0", - "web3-errors": "^1.1.3", - "web3-types": "^1.3.0", - "web3-validator": "^2.0.3" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "node_modules/web3-rpc-methods/node_modules/webidl-conversions": { - "version": "3.0.1", - "license": "BSD-2-Clause" - }, - "node_modules/web3-rpc-methods/node_modules/whatwg-url": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/web3-rpc-methods/node_modules/ws": { - "version": "8.14.2", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/web3-shh": { "version": "1.8.2", "hasInstallScript": true, @@ -107454,7 +106936,7 @@ }, "packages/commands": { "name": "@audius/commands", - "version": "1.0.27", + "version": "1.0.28", "license": "Apache-2.0", "dependencies": { "@audius/sdk": "*", @@ -107487,7 +106969,7 @@ }, "packages/common": { "name": "@audius/common", - "version": "1.5.72", + "version": "1.5.73", "dependencies": { "@audius/fixed-decimal": "*", "@audius/sdk": "*", @@ -107804,7 +107286,7 @@ }, "packages/compose": { "name": "@audius/compose", - "version": "1.0.27", + "version": "1.0.28", "license": "ISC", "dependencies": { "@audius/sdk": "*", @@ -107815,18 +107297,21 @@ } }, "packages/create-audius-app": { - "version": "1.0.2", + "version": "1.0.3", "license": "MIT", "bin": { "create-audius-app": "dist/index.js" }, "devDependencies": { + "@playwright/test": "1.42.1", + "@types/glob": "7.1.1", "@types/prompts": "2.4.2", "@types/tar": "6.1.11", "@types/validate-npm-package-name": "4.0.2", "commander": "2.20.0", "execa": "2.0.3", "fast-glob": "3.3.1", + "glob": "7.1.6", "prompts": "2.4.2", "tar": "6.2.0", "tsup": "8.0.2", @@ -108373,6 +107858,26 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "packages/create-audius-app/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "packages/create-audius-app/node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -110730,7 +110235,7 @@ }, "packages/ddex/publisher": { "name": "@audius/ddex-publisher", - "version": "0.0.9", + "version": "0.0.10", "dependencies": { "@audius/sdk": "*", "@aws-sdk/client-s3": "3.504.0", @@ -110758,7 +110263,7 @@ }, "packages/ddex/webapp/client": { "name": "@audius/ddex-webapp-client", - "version": "0.0.16", + "version": "0.0.17", "dependencies": { "@audius/harmony": "*", "@audius/sdk": "*", @@ -110868,7 +110373,7 @@ }, "packages/ddex/webapp/server": { "name": "@audius/ddex-webapp-server", - "version": "0.0.16", + "version": "0.0.17", "dependencies": { "@audius/sdk": "*", "@aws-sdk/client-s3": "3.504.0", @@ -110912,7 +110417,7 @@ }, "packages/discovery-provider/plugins/pedalboard/apps/app-template": { "name": "@pedalboard/app-template", - "version": "0.0.26", + "version": "0.0.27", "dependencies": { "@pedalboard/basekit": "*", "@pedalboard/logger": "*", @@ -113137,7 +112642,7 @@ }, "packages/discovery-provider/plugins/pedalboard/apps/relay": { "name": "@pedalboard/relay", - "version": "0.1.26", + "version": "0.1.27", "dependencies": { "@audius/sdk": "*", "@pedalboard/basekit": "*", @@ -116244,7 +115749,7 @@ }, "packages/discovery-provider/plugins/pedalboard/apps/sla-auditor": { "name": "@pedalboard/sla-auditor", - "version": "0.0.26", + "version": "0.0.27", "dependencies": { "@audius/sdk": "*", "@pedalboard/basekit": "*", @@ -118478,7 +117983,7 @@ }, "packages/discovery-provider/plugins/pedalboard/apps/solana-relay": { "name": "@pedalboard/solana-relay", - "version": "0.0.19", + "version": "0.0.20", "dependencies": { "@audius/spl": "*", "@pedalboard/basekit": "*", @@ -119794,7 +119299,7 @@ }, "packages/discovery-provider/plugins/pedalboard/apps/trending-challenge-rewards": { "name": "@pedalboard/trending-challenge-rewards", - "version": "0.0.26", + "version": "0.0.27", "dependencies": { "@audius/sdk": "*", "@pedalboard/basekit": "*", @@ -122037,7 +121542,7 @@ }, "packages/discovery-provider/plugins/pedalboard/packages/basekit": { "name": "@pedalboard/basekit", - "version": "0.0.26", + "version": "0.0.27", "dependencies": { "@pedalboard/storage": "*", "dayjs": "^1.11.7", @@ -123678,7 +123183,7 @@ } }, "packages/discovery-provider/plugins/pedalboard/packages/eslint-config-custom": { - "version": "0.0.26", + "version": "0.0.27", "license": "MIT", "dependencies": { "eslint-config-next": "^13.4.1", @@ -123688,7 +123193,7 @@ } }, "packages/discovery-provider/plugins/pedalboard/packages/eslint-config-custom-server": { - "version": "0.0.26", + "version": "0.0.27", "license": "MIT" }, "packages/discovery-provider/plugins/pedalboard/packages/eslint-config-custom/node_modules/doctrine": { @@ -123743,7 +123248,7 @@ } }, "packages/discovery-provider/plugins/pedalboard/packages/jest-presets": { - "version": "0.0.26", + "version": "0.0.27", "license": "MIT", "dependencies": { "ts-jest": "^26.5.0" @@ -123901,7 +123406,7 @@ }, "packages/discovery-provider/plugins/pedalboard/packages/logger": { "name": "@pedalboard/logger", - "version": "0.0.26", + "version": "0.0.27", "devDependencies": { "@types/jest": "^26.0.22", "eslint": "^7.32.0", @@ -125533,7 +125038,7 @@ }, "packages/discovery-provider/plugins/pedalboard/packages/storage": { "name": "@pedalboard/storage", - "version": "0.0.26", + "version": "0.0.27", "dependencies": { "knex": "^2.4.2", "knex-types": "^0.5.0" @@ -127168,7 +126673,7 @@ } }, "packages/discovery-provider/plugins/pedalboard/packages/tsconfig": { - "version": "0.0.26", + "version": "0.0.27", "license": "MIT" }, "packages/dotenv-lint": { @@ -127179,7 +126684,7 @@ }, "packages/dotenv-linter": { "name": "@audius/dotenv-linter", - "version": "1.5.71", + "version": "1.5.72", "hasInstallScript": true, "license": "Apache-2.0", "bin": { @@ -127187,7 +126692,7 @@ } }, "packages/embed": { - "version": "1.5.72", + "version": "1.5.73", "dependencies": { "@audius/fetch-nft": "0.1.8", "@audius/fixed-decimal": "*", @@ -127599,7 +127104,7 @@ } }, "packages/es-indexer": { - "version": "1.0.19", + "version": "1.0.20", "dependencies": { "@elastic/elasticsearch": "8.1.0", "commander": "9.2.0", @@ -127864,7 +127369,7 @@ } }, "packages/eslint-config-audius": { - "version": "1.5.72", + "version": "1.5.73", "license": "ISC", "peerDependencies": { "@emotion/eslint-plugin": "11.11.0", @@ -127894,7 +127399,7 @@ }, "packages/fixed-decimal": { "name": "@audius/fixed-decimal", - "version": "0.0.21", + "version": "0.0.22", "license": "Apache-2.0", "devDependencies": { "@types/bn.js": "5.1.0", @@ -128127,7 +127632,7 @@ }, "packages/harmony": { "name": "@audius/harmony", - "version": "0.0.30", + "version": "0.0.31", "license": "ISC", "dependencies": { "@emotion/css": "^11.11.2", @@ -129528,7 +129033,7 @@ "license": "ISC" }, "packages/identity-service": { - "version": "0.0.27", + "version": "0.0.28", "license": "Apache-2.0", "dependencies": { "@amplitude/node": "1.9.2", @@ -132285,12 +131790,12 @@ }, "packages/libs": { "name": "@audius/sdk", - "version": "3.0.40", + "version": "3.0.41", "license": "Apache-2.0", "dependencies": { - "@audius/fixed-decimal": "^0.0.21", + "@audius/fixed-decimal": "*", "@audius/hedgehog": "3.0.0-alpha.0", - "@audius/spl": "^0.0.27", + "@audius/spl": "*", "@babel/core": "^7.23.7", "@babel/plugin-proposal-class-static-block": "7.21.0", "@babel/runtime": "7.18.3", @@ -132435,18 +131940,6 @@ "node": ">=0.12" } }, - "packages/libs/node_modules/@audius/spl": { - "version": "0.0.27", - "resolved": "https://registry.npmjs.org/@audius/spl/-/spl-0.0.27.tgz", - "integrity": "sha512-nffNNwAtbzbKme1IeJtQCLp8CqIokEwBseaRtyfqe4xQO3wiiMf2Py/m2uqQxOdJQfG+MIIcnKSVejJTi9Hx1Q==", - "dependencies": { - "@coral-xyz/anchor": "0.29.0", - "@solana/buffer-layout": "4.0.1", - "@solana/buffer-layout-utils": "0.2.0", - "@solana/spl-token": "0.3.8", - "@solana/web3.js": "1.78.4" - } - }, "packages/libs/node_modules/@babel/core": { "version": "7.23.7", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", @@ -134209,7 +133702,7 @@ }, "packages/mobile": { "name": "audius-mobile-client", - "version": "1.5.72", + "version": "1.5.73", "dependencies": { "@amplitude/react-native": "2.17.2", "@audius/common": "*", @@ -136535,793 +136028,9 @@ "node": ">=6" } }, - "packages/probers": { - "version": "1.5.72", - "license": "ISC", - "dependencies": { - "@testing-library/cypress": "^9.0.0", - "cypress": "^13.0.0", - "cypress-file-upload": "^5.0.8", - "cypress-wait-until": "^1.7.2", - "dayjs": "^1.11.6", - "lodash": "4.17.21", - "web3": "4.1.1" - }, - "devDependencies": { - "cypress-plugin-tab": "^1.0.5", - "eslint": "8.56.0", - "eslint-plugin-cypress": "2.12.1", - "typescript": "5.0.4" - } - }, - "packages/probers/node_modules/@cypress/request": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", - "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "http-signature": "~1.3.6", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "performance-now": "^2.1.0", - "qs": "6.10.4", - "safe-buffer": "^5.1.2", - "tough-cookie": "^4.1.3", - "tunnel-agent": "^0.6.0", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "packages/probers/node_modules/@humanwhocodes/config-array": { - "version": "0.9.5", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "packages/probers/node_modules/@types/node": { - "version": "16.18.77", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.77.tgz", - "integrity": "sha512-zwqAbRkHjGlxH9PBv8i9dmeaDpBRgfQDSFuREMF2Z+WUi8uc13gfRquMV/8LxBqwm+7jBz+doTVkEEA1CIWOnQ==" - }, - "packages/probers/node_modules/@types/ws": { - "version": "8.5.3", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "packages/probers/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "packages/probers/node_modules/argparse": { - "version": "2.0.1", - "dev": true, - "license": "Python-2.0" - }, - "packages/probers/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "packages/probers/node_modules/chalk": { - "version": "4.1.2", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "packages/probers/node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "engines": { - "node": ">= 6" - } - }, - "packages/probers/node_modules/cross-fetch": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.12" - } - }, - "packages/probers/node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "packages/probers/node_modules/cypress": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.0.0.tgz", - "integrity": "sha512-nWHU5dUxP2Wm/zrMd8SWTTl706aJex/l+H4vi/tbu2SWUr17BUcd/sIYeqyxeoSPW1JFV2pT1pf4JEImH/POMg==", - "hasInstallScript": true, - "dependencies": { - "@cypress/request": "^3.0.0", - "@cypress/xvfb": "^1.2.4", - "@types/node": "^16.18.39", - "@types/sinonjs__fake-timers": "8.1.1", - "@types/sizzle": "^2.3.2", - "arch": "^2.2.0", - "blob-util": "^2.0.2", - "bluebird": "^3.7.2", - "buffer": "^5.6.0", - "cachedir": "^2.3.0", - "chalk": "^4.1.0", - "check-more-types": "^2.24.0", - "cli-cursor": "^3.1.0", - "cli-table3": "~0.6.1", - "commander": "^6.2.1", - "common-tags": "^1.8.0", - "dayjs": "^1.10.4", - "debug": "^4.3.4", - "enquirer": "^2.3.6", - "eventemitter2": "6.4.7", - "execa": "4.1.0", - "executable": "^4.1.1", - "extract-zip": "2.0.1", - "figures": "^3.2.0", - "fs-extra": "^9.1.0", - "getos": "^3.2.1", - "is-ci": "^3.0.0", - "is-installed-globally": "~0.4.0", - "lazy-ass": "^1.6.0", - "listr2": "^3.8.3", - "lodash": "^4.17.21", - "log-symbols": "^4.0.0", - "minimist": "^1.2.8", - "ospath": "^1.2.2", - "pretty-bytes": "^5.6.0", - "process": "^0.11.10", - "proxy-from-env": "1.0.0", - "request-progress": "^3.0.0", - "semver": "^7.5.3", - "supports-color": "^8.1.1", - "tmp": "~0.2.1", - "untildify": "^4.0.0", - "yauzl": "^2.10.0" - }, - "bin": { - "cypress": "bin/cypress" - }, - "engines": { - "node": "^16.0.0 || ^18.0.0 || >=20.0.0" - } - }, - "packages/probers/node_modules/cypress/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "packages/probers/node_modules/dayjs": { - "version": "1.11.10", - "license": "MIT" - }, - "packages/probers/node_modules/eslint": { - "version": "8.19.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint/eslintrc": "^1.3.0", - "@humanwhocodes/config-array": "^0.9.2", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.2", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^6.0.1", - "globals": "^13.15.0", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "packages/probers/node_modules/eslint-scope": { - "version": "7.2.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "packages/probers/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "packages/probers/node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "packages/probers/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "packages/probers/node_modules/glob-parent": { - "version": "6.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "packages/probers/node_modules/globals": { - "version": "13.23.0", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/probers/node_modules/js-yaml": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "packages/probers/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "packages/probers/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "packages/probers/node_modules/node-fetch": { - "version": "2.7.0", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "packages/probers/node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "packages/probers/node_modules/proxy-from-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", - "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==" - }, - "packages/probers/node_modules/qs": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", - "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "packages/probers/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "packages/probers/node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "packages/probers/node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "packages/probers/node_modules/supports-color": { - "version": "7.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "packages/probers/node_modules/tr46": { - "version": "0.0.3", - "license": "MIT" - }, - "packages/probers/node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/probers/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "engines": { - "node": ">= 10.0.0" - } - }, - "packages/probers/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "packages/probers/node_modules/web3": { - "version": "4.1.1", - "license": "LGPL-3.0", - "dependencies": { - "web3-core": "^4.1.1", - "web3-errors": "^1.1.1", - "web3-eth": "^4.1.1", - "web3-eth-abi": "^4.1.1", - "web3-eth-accounts": "^4.0.5", - "web3-eth-contract": "^4.0.5", - "web3-eth-ens": "^4.0.5", - "web3-eth-iban": "^4.0.5", - "web3-eth-personal": "^4.0.5", - "web3-net": "^4.0.5", - "web3-providers-http": "^4.0.5", - "web3-providers-ws": "^4.0.5", - "web3-rpc-methods": "^1.1.1", - "web3-types": "^1.1.1", - "web3-utils": "^4.0.5", - "web3-validator": "^2.0.1" - }, - "engines": { - "node": ">=14.0.0", - "npm": ">=6.12.0" - } - }, - "packages/probers/node_modules/web3-core": { - "version": "4.3.1", - "license": "LGPL-3.0", - "dependencies": { - "web3-errors": "^1.1.4", - "web3-eth-iban": "^4.0.7", - "web3-providers-http": "^4.1.0", - "web3-providers-ws": "^4.0.7", - "web3-types": "^1.3.1", - "web3-utils": "^4.0.7", - "web3-validator": "^2.0.3" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - }, - "optionalDependencies": { - "web3-providers-ipc": "^4.0.7" - } - }, - "packages/probers/node_modules/web3-eth": { - "version": "4.3.1", - "license": "LGPL-3.0", - "dependencies": { - "setimmediate": "^1.0.5", - "web3-core": "^4.3.0", - "web3-errors": "^1.1.3", - "web3-eth-abi": "^4.1.4", - "web3-eth-accounts": "^4.1.0", - "web3-net": "^4.0.7", - "web3-providers-ws": "^4.0.7", - "web3-rpc-methods": "^1.1.3", - "web3-types": "^1.3.0", - "web3-utils": "^4.0.7", - "web3-validator": "^2.0.3" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "packages/probers/node_modules/web3-eth-abi": { - "version": "4.1.4", - "license": "LGPL-3.0", - "dependencies": { - "abitype": "0.7.1", - "web3-errors": "^1.1.3", - "web3-types": "^1.3.0", - "web3-utils": "^4.0.7", - "web3-validator": "^2.0.3" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "packages/probers/node_modules/web3-eth-accounts": { - "version": "4.1.0", - "license": "LGPL-3.0", - "dependencies": { - "@ethereumjs/rlp": "^4.0.1", - "crc-32": "^1.2.2", - "ethereum-cryptography": "^2.0.0", - "web3-errors": "^1.1.3", - "web3-types": "^1.3.0", - "web3-utils": "^4.0.7", - "web3-validator": "^2.0.3" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "packages/probers/node_modules/web3-eth-contract": { - "version": "4.1.3", - "license": "LGPL-3.0", - "dependencies": { - "web3-core": "^4.3.1", - "web3-errors": "^1.1.4", - "web3-eth": "^4.3.1", - "web3-eth-abi": "^4.1.4", - "web3-types": "^1.3.1", - "web3-utils": "^4.0.7", - "web3-validator": "^2.0.3" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "packages/probers/node_modules/web3-eth-ens": { - "version": "4.0.8", - "license": "LGPL-3.0", - "dependencies": { - "@adraffy/ens-normalize": "^1.8.8", - "web3-core": "^4.3.0", - "web3-errors": "^1.1.3", - "web3-eth": "^4.3.1", - "web3-eth-contract": "^4.1.2", - "web3-net": "^4.0.7", - "web3-types": "^1.3.0", - "web3-utils": "^4.0.7", - "web3-validator": "^2.0.3" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "packages/probers/node_modules/web3-eth-iban": { - "version": "4.0.7", - "license": "LGPL-3.0", - "dependencies": { - "web3-errors": "^1.1.3", - "web3-types": "^1.3.0", - "web3-utils": "^4.0.7", - "web3-validator": "^2.0.3" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "packages/probers/node_modules/web3-eth-personal": { - "version": "4.0.8", - "license": "LGPL-3.0", - "dependencies": { - "web3-core": "^4.3.0", - "web3-eth": "^4.3.1", - "web3-rpc-methods": "^1.1.3", - "web3-types": "^1.3.0", - "web3-utils": "^4.0.7", - "web3-validator": "^2.0.3" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "packages/probers/node_modules/web3-net": { - "version": "4.0.7", - "license": "LGPL-3.0", - "dependencies": { - "web3-core": "^4.3.0", - "web3-rpc-methods": "^1.1.3", - "web3-types": "^1.3.0", - "web3-utils": "^4.0.7" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "packages/probers/node_modules/web3-providers-http": { - "version": "4.1.0", - "license": "LGPL-3.0", - "dependencies": { - "cross-fetch": "^4.0.0", - "web3-errors": "^1.1.3", - "web3-types": "^1.3.0", - "web3-utils": "^4.0.7" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "packages/probers/node_modules/web3-providers-ipc": { - "version": "4.0.7", - "license": "LGPL-3.0", - "optional": true, - "dependencies": { - "web3-errors": "^1.1.3", - "web3-types": "^1.3.0", - "web3-utils": "^4.0.7" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "packages/probers/node_modules/web3-providers-ws": { - "version": "4.0.7", - "license": "LGPL-3.0", - "dependencies": { - "@types/ws": "8.5.3", - "isomorphic-ws": "^5.0.0", - "web3-errors": "^1.1.3", - "web3-types": "^1.3.0", - "web3-utils": "^4.0.7", - "ws": "^8.8.1" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "packages/probers/node_modules/web3-utils": { - "version": "4.0.7", - "license": "LGPL-3.0", - "dependencies": { - "ethereum-cryptography": "^2.0.0", - "web3-errors": "^1.1.3", - "web3-types": "^1.3.0", - "web3-validator": "^2.0.3" - }, - "engines": { - "node": ">=14", - "npm": ">=6.12.0" - } - }, - "packages/probers/node_modules/webidl-conversions": { - "version": "3.0.1", - "license": "BSD-2-Clause" - }, - "packages/probers/node_modules/whatwg-url": { - "version": "5.0.0", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "packages/probers/node_modules/ws": { - "version": "8.14.2", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "packages/probers/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "packages/spl": { "name": "@audius/spl", - "version": "0.0.28", + "version": "0.0.29", "license": "Apache-2.0", "dependencies": { "@coral-xyz/anchor": "0.29.0", @@ -137336,7 +136045,7 @@ }, "packages/sql-ts": { "name": "@audius/sql-ts", - "version": "1.0.30", + "version": "1.0.31", "license": "ISC", "devDependencies": { "@rmp135/sql-ts": "1.18.0", @@ -137465,7 +136174,7 @@ }, "packages/trpc-server": { "name": "@audius/trpc-server", - "version": "0.0.28", + "version": "0.0.29", "dependencies": { "@elastic/elasticsearch": "8.11.0", "@trpc/server": "10.38.4", @@ -138188,7 +136897,7 @@ }, "packages/web": { "name": "audius-client", - "version": "1.5.72", + "version": "1.5.73", "dependencies": { "@audius/common": "*", "@audius/fixed-decimal": "*", @@ -138276,7 +136985,6 @@ "numeral": "2.0.6", "orbit-controls": "0.0.1", "perspective-camera": "2.0.1", - "pixi.js": "4.8.9", "prop-types": "15.7.2", "query-string": "6.13.5", "raw-loader": "0.5.1", @@ -138333,6 +137041,7 @@ "@electron/notarize": "2.2.0", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@pinata/sdk": "1.1.13", + "@playwright/test": "1.42.1", "@redux-devtools/extension": "3.2.4", "@testing-library/react": "12.0.0", "@types/amplitude-js": "8.0.2", @@ -140824,7 +139533,7 @@ }, "protocol-dashboard": { "name": "audius-protocol-dashboard", - "version": "0.1.9", + "version": "0.1.10", "dependencies": { "@apollo/client": "3.3.7", "@audius/common": "*", diff --git a/package.json b/package.json index 5ed1e4ad342..f6d8e0f0acd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.5.72", + "version": "1.5.73", "workspaces": [ "packages/*", "packages/discovery-provider/plugins/pedalboard/apps/*", @@ -30,7 +30,7 @@ "web:dev": "npm run start:dev -w audius-client", "web:prod": "npm run start:prod -w audius-client", "web:stage": "npm run start:stage -w audius-client", - "web:e2e": "concurrently -k 'npm run start:stage -w audius-client' 'npm run cypress:open -w probers' ", + "web:e2e": "npm run e2e -w audius-client", "DESKTOP====================================": "", "desktop:dev": "concurrently -k 'BROWSER=none npm run start:dev -w audius-client' 'wait-on http://0.0.0.0:3000 && npm run electron:localhost -w audius-client -- 3000'", "desktop:prod": "concurrently -k 'BROWSER=none npm run start:prod -w audius-client' 'wait-on http://0.0.0.0:3002 && npm run electron:localhost -w audius-client -- 3002'", diff --git a/packages/commands/package.json b/packages/commands/package.json index 4ed3bebc956..d625b2148d9 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -1,6 +1,6 @@ { "name": "@audius/commands", - "version": "1.0.27", + "version": "1.0.28", "private": true, "description": "Collection of command useful for debugging audius-protocol", "main": "src/index.mjs", diff --git a/packages/common/package.json b/packages/common/package.json index ba10706e4c7..eb341210054 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@audius/common", - "version": "1.5.72", + "version": "1.5.73", "description": "Common utilities and store for web and mobile.", "private": true, "author": "Audius", diff --git a/packages/common/src/messages/sign-on/pages.ts b/packages/common/src/messages/sign-on/pages.ts index e878f2e78b4..3cf6040dbc2 100644 --- a/packages/common/src/messages/sign-on/pages.ts +++ b/packages/common/src/messages/sign-on/pages.ts @@ -26,11 +26,6 @@ export const createEmailPageMessages = { signUp: 'Sign Up Free', haveAccount: 'Already have an account?', signIn: 'Sign In', - subHeader: { - // Two separate lines separated by a divider. Can't include the divider here since its different for native vs web - line1: 'Join the revolution in music streaming!', - line2: 'Discover, connect, and create on Audius.' - }, socialsDividerText: 'Or, get started with one of your socials', unknownError: 'Unknown error occurred.', metaMaskNotRecommended: 'Signing up with MetaMask is not recommended.', diff --git a/packages/common/src/services/opensea-client/OpenSeaClient.ts b/packages/common/src/services/opensea-client/OpenSeaClient.ts index 6191f44c53d..a5f69648843 100644 --- a/packages/common/src/services/opensea-client/OpenSeaClient.ts +++ b/packages/common/src/services/opensea-client/OpenSeaClient.ts @@ -44,7 +44,7 @@ export class OpenSeaClient { events = json.asset_events while (next) { res = await fetch( - `${this.url}/api/v2/events?account=${wallet}&limit=${limit}&event_type=transfer&chain=ethereum` + `${this.url}/api/v2/events?account=${wallet}&limit=${limit}&event_type=transfer&chain=ethereum&next=${next}` ) json = await res.json() next = json.next @@ -92,7 +92,7 @@ export class OpenSeaClient { nfts = json.nfts while (next) { res = await fetch( - `${this.url}/api/v2/chain/ethereum/account/${wallet}/nfts` + `${this.url}/api/v2/chain/ethereum/account/${wallet}/nfts?next=${next}` ) json = await res.json() next = json.next diff --git a/packages/compose/package.json b/packages/compose/package.json index 913ad977804..503b5eb25b4 100644 --- a/packages/compose/package.json +++ b/packages/compose/package.json @@ -1,6 +1,6 @@ { "name": "@audius/compose", - "version": "1.0.27", + "version": "1.0.28", "private": true, "scripts": { "start": "./start.sh" diff --git a/packages/create-audius-app/examples/react/index.html b/packages/create-audius-app/examples/react/index.html index 146f474bb65..30f4f50deff 100644 --- a/packages/create-audius-app/examples/react/index.html +++ b/packages/create-audius-app/examples/react/index.html @@ -5,6 +5,10 @@ Vite + React + TS +
diff --git a/packages/create-audius-app/examples/react/src/App.tsx b/packages/create-audius-app/examples/react/src/App.tsx index 16d5aebb6a3..32f80eb36fa 100644 --- a/packages/create-audius-app/examples/react/src/App.tsx +++ b/packages/create-audius-app/examples/react/src/App.tsx @@ -94,20 +94,22 @@ export default function App() { } /** - * Favorite a track. This requires a user to be authenticated and granted + * Favorite or unfavorite a track. This requires a user to be authenticated and granted * write permissions to the app */ const favoriteTrack = - (trackId: string): MouseEventHandler => + (trackId: string, favorite = true): MouseEventHandler => async (e) => { e.stopPropagation() if (user) { - setFavorites((prev) => ({ ...prev, [trackId]: true })) + setFavorites((prev) => ({ ...prev, [trackId]: favorite })) try { - await audiusSdk.tracks.favoriteTrack({ userId: user.userId, trackId }) + await audiusSdk.tracks[ + favorite ? 'favoriteTrack' : 'unfavoriteTrack' + ]({ userId: user.userId, trackId }) } catch (e) { console.error('Failed to favorite track', e) - setFavorites((prev) => ({ ...prev, [trackId]: false })) + setFavorites((prev) => ({ ...prev, [trackId]: !favorite })) } } else { alert('Please log in with Audius to perform write operations') @@ -197,17 +199,13 @@ export default function App() { {track.title} {track.user.name} - {!favorites[track.id] ? ( - - ) : ( - Favorited! - )} + ))} diff --git a/packages/create-audius-app/helpers/copy.ts b/packages/create-audius-app/helpers/copy.ts index 95f6fff2fab..de17853d488 100644 --- a/packages/create-audius-app/helpers/copy.ts +++ b/packages/create-audius-app/helpers/copy.ts @@ -25,7 +25,8 @@ export const copy = async ( cwd, dot: true, absolute: false, - stats: false + stats: false, + ignore: ['node_modules/**'] }) const destRelativeToCwd = cwd ? path.resolve(cwd, dest) : dest diff --git a/packages/create-audius-app/package.json b/packages/create-audius-app/package.json index a7277dbceb9..8e757ff4813 100644 --- a/packages/create-audius-app/package.json +++ b/packages/create-audius-app/package.json @@ -1,15 +1,18 @@ { "name": "create-audius-app", - "version": "1.0.2", + "version": "1.0.3", "description": "Create an Audius app with one command", "scripts": { "build": "tsup ./index.ts", "dev": "tsup ./index.ts --watch", "lint": "eslint", "prepublishOnly": "rm -rf dist && npm run build", - "test": "vitest", + "setup-test-env": "./tests/e2e/setup-test-env.sh", + "test": "vitest tests/integration", + "test:e2e": "playwright test", "typecheck": "tsc", - "verify": "concurrently \"npm:typecheck\" \"npm:lint\"" + "verify": "concurrently \"npm:typecheck\" \"npm:lint\"", + "start-examples": "concurrently \"cd examples/react && npm run start\"" }, "repository": { "type": "git", @@ -31,12 +34,15 @@ "examples" ], "devDependencies": { + "@playwright/test": "1.42.1", + "@types/glob": "7.1.1", "@types/prompts": "2.4.2", "@types/tar": "6.1.11", "@types/validate-npm-package-name": "4.0.2", "commander": "2.20.0", "execa": "2.0.3", "fast-glob": "3.3.1", + "glob": "7.1.6", "prompts": "2.4.2", "tar": "6.2.0", "tsup": "8.0.2", diff --git a/packages/create-audius-app/playwright.config.ts b/packages/create-audius-app/playwright.config.ts new file mode 100644 index 00000000000..66825ea3227 --- /dev/null +++ b/packages/create-audius-app/playwright.config.ts @@ -0,0 +1,37 @@ +import { defineConfig, devices } from '@playwright/test' +import fs from 'fs' + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests/e2e', + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 1 : 0, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI + ? [ + ['junit', { outputFile: 'report.xml', outputDir: 'playwright-report' }], + ['html', { open: 'never' }], + ['line'], + ['blob'] + ] + : 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry' + }, + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npm run start-examples', + url: 'http://localhost:4173', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + timeout: 60000 * 5 + } +}) diff --git a/packages/create-audius-app/tests/e2e/react.test.ts b/packages/create-audius-app/tests/e2e/react.test.ts new file mode 100644 index 00000000000..48fc2a17f84 --- /dev/null +++ b/packages/create-audius-app/tests/e2e/react.test.ts @@ -0,0 +1,66 @@ +import { test, expect } from '@playwright/test' + +test('auths, fetches tracks, and favorites a track', async ({ + page, + context +}) => { + test.slow() + + // Set entropy so we don't need to do OTP + const entropy = process.env.CREATE_AUDIUS_APP_TEST_ENTROPY ?? '' + await context.addInitScript((entropy) => { + if (window.location.hostname === 'audius.co') { + window.localStorage.setItem('hedgehog-entropy-key', entropy) + } + }, entropy) + + await page.goto('localhost:4173') + + const pagePromise = context.waitForEvent('page') + await page.getByRole('button', { name: 'Continue With Audius' }).click() + + // Oauth popup + const authPage = await pagePromise + await authPage.waitForLoadState() + await authPage.getByRole('button', { name: 'Continue' }).click() + + await expect(page.getByText('Continue With Audius')).not.toBeVisible() + + // Fetch tracks + await page.getByRole('textbox').fill('createaudiusapptracks') + await page.getByRole('button', { name: 'Get Tracks' }).click() + + // Set up block_confirmation response listener + const responsePromise = page.waitForResponse(async (response) => { + if (response.url().includes('block_confirmation')) { + const json = await response.json() + return json.data.block_passed + } + }) + + const favoriteButton = page + .getByRole('button', { name: 'Favorite', exact: true }) + .first() + const unfavoriteButton = page + .getByRole('button', { name: 'Unfavorite', exact: true }) + .first() + + // Either favorite or unfavorite the track + const favoriteButtonExists = await Promise.any([ + favoriteButton.waitFor().then(() => true), + unfavoriteButton.waitFor().then(() => false) + ]).catch(() => { + throw 'Missing button' + }) + + if (favoriteButtonExists) { + await favoriteButton.click() + await expect(unfavoriteButton).toBeVisible() + } else { + await unfavoriteButton.click() + await expect(favoriteButton).toBeVisible() + } + + // Confirm track is updated + await responsePromise +}) diff --git a/packages/create-audius-app/tests/e2e/setup-test-env.sh b/packages/create-audius-app/tests/e2e/setup-test-env.sh new file mode 100755 index 00000000000..b9ff871f56c --- /dev/null +++ b/packages/create-audius-app/tests/e2e/setup-test-env.sh @@ -0,0 +1,4 @@ +# Replace the API key in the example app with a test key +find ./examples/react/src/App.tsx -type f -exec sed -i.bak "s/\/\/ apiKey: \"Your API Key goes here\"/,apiKey: \"$CREATE_AUDIUS_APP_TEST_API_KEY\"/g" {} \; +find ./examples/react/src/App.tsx -type f -exec sed -i.bak "s/\/\/ apiSecret: \"Your API Secret goes here\"/apiSecret: \"$CREATE_AUDIUS_APP_TEST_API_SECRET\"/g" {} \; +rm ./examples/react/src/App.tsx.bak \ No newline at end of file diff --git a/packages/create-audius-app/index.test.ts b/packages/create-audius-app/tests/integration/create-audius-app.test.ts similarity index 96% rename from packages/create-audius-app/index.test.ts rename to packages/create-audius-app/tests/integration/create-audius-app.test.ts index dc3477b7d8f..8c8b8fc97c7 100644 --- a/packages/create-audius-app/index.test.ts +++ b/packages/create-audius-app/tests/integration/create-audius-app.test.ts @@ -1,12 +1,9 @@ import execa from 'execa' import fs from 'fs-extra' import path from 'path' -import { useTempDir } from './test/use-temp-dir' +import { useTempDir } from './use-temp-dir' import { describe, it, expect } from 'vitest' -import { - projectFilesShouldExist, - projectFilesShouldNotExist -} from './test/utils' +import { projectFilesShouldExist, projectFilesShouldNotExist } from './utils' const cli = require.resolve('create-audius-app/dist/index.js') diff --git a/packages/create-audius-app/test/use-temp-dir.ts b/packages/create-audius-app/tests/integration/use-temp-dir.ts similarity index 100% rename from packages/create-audius-app/test/use-temp-dir.ts rename to packages/create-audius-app/tests/integration/use-temp-dir.ts diff --git a/packages/create-audius-app/test/utils.ts b/packages/create-audius-app/tests/integration/utils.ts similarity index 100% rename from packages/create-audius-app/test/utils.ts rename to packages/create-audius-app/tests/integration/utils.ts diff --git a/packages/create-audius-app/tsconfig.json b/packages/create-audius-app/tsconfig.json index b3b269153d7..741e9969702 100644 --- a/packages/create-audius-app/tsconfig.json +++ b/packages/create-audius-app/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "lib": ["esnext"], "target": "es2019", "moduleResolution": "node", "strict": true, diff --git a/packages/ddex/.env.dev b/packages/ddex/.env.dev index c0f09caeac9..0824bba94a9 100644 --- a/packages/ddex/.env.dev +++ b/packages/ddex/.env.dev @@ -4,7 +4,8 @@ AWS_ACCESS_KEY_ID='test' AWS_SECRET_ACCESS_KEY='test' AWS_REGION='us-west-2' AWS_BUCKET_RAW='audius-test-raw' -AWS_BUCKET_INDEXED='audius-test-crawled' +AWS_BUCKET_CRAWLED='audius-test-crawled' +AWS_ENDPOINT='http://ingress:4566' DDEX_KEY='49d5e13d355709b615b7cce7369174fb240b6b39' DDEX_SECRET='2b2c2b90d9a489234ae629a5284de84fb0633306257f17667aaebf2345d92152' @@ -12,9 +13,5 @@ DDEX_ADMIN_ALLOWLIST='127559427' SESSION_SECRET='something random' -AWS_ENDPOINT='http://ddex-s3-release-by-release:4566' +# Change to 'ERNBatched' to use batched choreography DDEX_CHOREOGRAPHY='ERNReleaseByRelease' - -# Uncomment these and comment the 2 lines above to use batched choreography -# AWS_ENDPOINT='http://ddex-s3-batched:4566' -# DDEX_CHOREOGRAPHY='ERNBatched' diff --git a/packages/ddex/.env.stage b/packages/ddex/.env.stage index 797c803c455..9ca44de7e8a 100644 --- a/packages/ddex/.env.stage +++ b/packages/ddex/.env.stage @@ -4,7 +4,7 @@ AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_REGION='us-west-2' AWS_BUCKET_RAW='ddex-dev-audius-raw' -AWS_BUCKET_INDEXED='ddex-dev-audius-indexed' +AWS_BUCKET_CRAWLED='ddex-dev-audius-crawled' DDEX_KEY='49d5e13d355709b615b7cce7369174fb240b6b39' DDEX_SECRET='2b2c2b90d9a489234ae629a5284de84fb0633306257f17667aaebf2345d92152' diff --git a/packages/ddex/README.md b/packages/ddex/README.md index 177db3439df..c684e942e8b 100644 --- a/packages/ddex/README.md +++ b/packages/ddex/README.md @@ -84,12 +84,12 @@ For docker compose to work: `cat packages/ddex/.env >> dev-tools/compose/.env` ### Bring up the ddex stack locally -Run `audius-compose up -ddex-release-by-released` (or `audius-compose up --ddex-batched` -- see "Choreography" in Glossary below), and navigate to `http://localhost:9000` to view the DDEX webapp +Run `audius-compose up -ddex-release-by-release` (or `audius-compose up --ddex-batched` -- see "Choreography" in Glossary below), and navigate to `http://localhost:9000` to view the DDEX webapp To upload a delivery to be processed: - 1. Create buckets: `aws --endpoint=http://localhost:4566 s3 mb s3://audius-test-raw && aws --endpoint=http://localhost:4566 s3 mb s3://audius-test-crawled` - 2. Upload your file: `aws --endpoint=http://localhost:4566 s3 cp s3://audius-test-raw`. Example: `aws --endpoint=http://localhost:4566 s3 cp ./ingester/e2e_test/fixtures/release_by_release/ern381/sony1.zip s3://audius-test-raw` - 3. Wait 3 minutes and it'll be processed and display in the UI (localhost:9000) + 1. Create buckets: `aws --endpoint=http://ingress:4566 s3 mb s3://audius-test-raw && aws --endpoint=http://ingress:4566 s3 mb s3://audius-test-crawled` + 2. Upload your file: `aws --endpoint=http://ingress:4566 s3 cp s3://audius-test-raw`. Example: `aws --endpoint=http://ingress:4566 s3 cp ./ingester/e2e_test/fixtures/release_by_release/ern381/sony1.zip s3://audius-test-raw` + 3. Watch the UI (localhost:9000) for the delivery to be crawled in a few seconds To access the ddex db via the mongo shell: `docker exec -it ddex-mongo mongosh -u mongo -p mongo --authenticationDatabase admin`, and then `use ddex`. @@ -97,12 +97,8 @@ To access the ddex db via the mongo shell: `docker exec -it ddex-mongo mongosh - Each service can be run independently as long as `ddex-mongo` is up (from `audius-compose up --ddex-[release-by-release|batched]` and then optionally stopping individual services). See the respective subdirectories' READMEs. ### Running / debugging the e2e test -* Run `audius-compose test down && audius-compose test run ddex-e2e-release-by-release` to start the ddex stack and run the e2e test for the Release-By-Release choreography. You can replace `ddex-e2e-release-by-release` with `ddex-e2e-batched` to run the e2e test for the Batched choreography. -* To debug S3: - 1. Exec into `ddex-s3-release-by-release` (or `ddex-s3-batched`) - 2. Run `pip install awscli` - 3. Run `aws configure` and enter `test` as both credentials and `us-west-2` as the region when prompted - 4. You can now run `aws --endpoint=http://localhost:4566 s3 ls` and other commands to debug the S3 state +* Run `audius-compose test down && audius-compose test run ddex-e2e-release-by-release` to start the ddex stack and run the e2e test for the Release-By-Release choreography. Or run `audius-compose test run ddex-e2e-batched` to run the e2e test for the Batched choreography. +* To debug S3, follow the onte-time setup instructions above to update your `/etc/hosts` and install the AWS cli. Then you can run `aws --endpoint=http://localhost:4566 s3 ls` and other commands to debug the S3 state. ## App architecture and flows 1. A distributor uploads a ZIP file to the "raw" AWS S3 bucket. diff --git a/packages/ddex/ingester/README.md b/packages/ddex/ingester/README.md index 52913d24e82..9c2d4df0067 100644 --- a/packages/ddex/ingester/README.md +++ b/packages/ddex/ingester/README.md @@ -1,11 +1,12 @@ # Audius DDEX Ingester -Indexes and parses new DDEX uploads. +Crawls and parses new DDEX uploads. ### Local Dev -`crawler`, `indexer`, and `parser` are independent services. Each handles a stage in the DDEX ingestion pipeline. +`crawler` and `parser` are independent ingester services. Each handles a stage in the DDEX ingestion pipeline. -To run an ingester service locally with hot reloading: -1. Make sure you can connect to mongo at `mongodb://mongo:mongo@localhost:27017/ddex`. See `packages/ddex/README.md` on how to spin up `ddex-mongo` and the other ddex containers. -2. Make sure you've configured your `packages/ddex/.env` and S3 buckets according to the toplevel DDEX README. -2. `air -c .air.toml -- --service [crawler|indexer|parser]` +The easiest way to test DDEX locally is via `audius-compose up --ddex-[release-by-release|batched]`. If you want to enable hot reloading for an ingester service: + +1. Make sure the DDEX stack is running. See `packages/ddex/README.md` for instructions on how to bring up the DDEX stack locally. +2. `docker stop ddex-crawler` or `docker stop ddex-parser` (assuming it's running as part of the whole DDEX stack) +3. `IS_DEV=true AWS_ENDPOINT=http://ingress:4566 DDEX_CHOREOGRAPHY=ERNReleaseByRelease air -c .air.toml -- --service [crawler|indexer|parser]` diff --git a/packages/ddex/ingester/artistutils/artistutils.go b/packages/ddex/ingester/artistutils/artistutils.go index 8f6c5928fa0..948b7860cd6 100644 --- a/packages/ddex/ingester/artistutils/artistutils.go +++ b/packages/ddex/ingester/artistutils/artistutils.go @@ -36,7 +36,6 @@ func GetFirstArtistID(artists []common.ResourceContributor, usersColl *mongo.Col artistID := "" artistName := "" - var err error var warnings []string for _, artist := range artists { filter := bson.M{"name": artist.Name} @@ -95,7 +94,7 @@ func GetFirstArtistID(artists []common.ResourceContributor, usersColl *mongo.Col if artistID != "" { return artistID, artistName, warnings, nil } - return "", "", warnings, err + return "", "", warnings, errors.New("no artist was found with the given display name - see warning for details") } // searchArtistOnAudius searches Audius (public /v1/users/search endpoint) for an artist by name and returns potential matches. diff --git a/packages/ddex/ingester/crawler/crawler.go b/packages/ddex/ingester/crawler/crawler.go index 4390f268f1d..d1e0e5f61d3 100644 --- a/packages/ddex/ingester/crawler/crawler.go +++ b/packages/ddex/ingester/crawler/crawler.go @@ -35,6 +35,8 @@ func RunNewCrawler(ctx context.Context) { interval := 3 * time.Minute if os.Getenv("TEST_MODE") == "true" { interval = time.Second + } else if os.Getenv("IS_DEV") == "true" { + interval = 5 * time.Second } ticker := time.NewTicker(interval) defer ticker.Stop() diff --git a/packages/ddex/ingester/e2e_test/e2e_test.go b/packages/ddex/ingester/e2e_test/e2e_test.go index 953f8a68653..6a22ac812d5 100644 --- a/packages/ddex/ingester/e2e_test/e2e_test.go +++ b/packages/ddex/ingester/e2e_test/e2e_test.go @@ -16,45 +16,21 @@ import ( "github.com/aws/aws-sdk-go/service/s3/s3manager" "github.com/stretchr/testify/assert" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" ) -func TestRunE2E(t *testing.T) { - e := &e2eTest{ - BaseIngester: common.NewBaseIngester(context.Background(), "test_e2e"), - } - defer e.MongoClient.Disconnect(e.Ctx) - - err := e.setupEnv() - if err != nil { - t.Fatalf("Failed to set up test environment: %v", err) - } - - choreography := common.MustGetChoreography() - if choreography == constants.ERNReleaseByRelease { - e.runERN381ReleaseByRelease(t) - } else if choreography == constants.ERNBatched { - e.runERN382Batched(t) - } else { - t.Fatalf("Unexpected choreography: %s", choreography) - } -} - -type e2eTest struct { - *common.BaseIngester -} - -func (e *e2eTest) setupEnv() error { - if err := createBucket(e.S3Client, e.RawBucket); err != nil { +func setupEnv(bi *common.BaseIngester) error { + if err := createBucket(bi.S3Client, bi.RawBucket); err != nil { return err } - if err := createBucket(e.S3Client, e.CrawledBucket); err != nil { + if err := createBucket(bi.S3Client, bi.CrawledBucket); err != nil { return err } - fmt.Printf("Created buckets: %s, %s\n", e.RawBucket, e.CrawledBucket) + fmt.Printf("Created buckets: %s, %s\n", bi.RawBucket, bi.CrawledBucket) - users := e.MongoClient.Database("ddex").Collection("users") - _, err := users.InsertOne(e.Ctx, bson.M{ + users := bi.MongoClient.Database("ddex").Collection("users") + _, err := users.InsertOne(bi.Ctx, bson.M{ "_id": "Bmv3bJ", "decodedUserId": "130821286", "handle": "theo_random", @@ -68,7 +44,7 @@ func (e *e2eTest) setupEnv() error { return fmt.Errorf("failed to insert user into Mongo: %v", err) } - _, err = users.InsertOne(e.Ctx, bson.M{ + _, err = users.InsertOne(bi.Ctx, bson.M{ "_id": "abcdef", "decodedUserId": "12345", "handle": "Monkey Claw", @@ -85,509 +61,565 @@ func (e *e2eTest) setupEnv() error { return nil } -// TODO: Turn this into a table test -func (e *e2eTest) runERN382Batched(t *testing.T) { - e.Logger.Info("Starting E2E test for ERN 382 Batched choreography") - - e.uploadFixture(t, "batch/ern382/CPD1.zip") - eTag := "5e425b53234b868374c0b02e0b58b1cc" +func TestRunE2E(t *testing.T) { + bi := common.NewBaseIngester(context.Background(), "test_e2e") + defer bi.MongoClient.Disconnect(bi.Ctx) - // Verify the crawler (deliveries collection) - doc, err := waitForDocument(e.Ctx, e.DeliveriesColl, bson.M{"_id": eTag}) + err := setupEnv(bi) if err != nil { - t.Fatalf("Error finding delivery in Mongo: %v", err) - } - if doc.Err() == mongo.ErrNoDocuments { - t.Fatalf("No delivery was found in Mongo: %v", doc.Err()) - } - var delivery common.Delivery - if err = doc.Decode(&delivery); err != nil { - t.Fatalf("Failed to decode delivery from Mongo: %v", err) + t.Fatalf("Failed to set up test environment: %v", err) } - assert.Equal(t, "s3://audius-test-raw/CPD1.zip", delivery.ZIPFilePath, "Path doesn't match expected") - assert.Equal(t, delivery.ZIPFileETag, eTag, "ETag (delivery ID) doesn't match expected") - assert.Equal(t, constants.DeliveryStatusParsing, delivery.DeliveryStatus, "delivery_status doesn't match expected") - assert.Equal(t, 0, len(delivery.Releases), "Expected 0 root releases in delivery") - assert.Equal(t, 1, len(delivery.Batches), "Expected 1 batch in delivery") - assert.Equal(t, "20161024145603121/BatchComplete_20161024145603121.xml", delivery.Batches[0].BatchXmlPath, "Batch XML path doesn't match expected") - assert.Equal(t, 1, len(delivery.Batches[0].Releases), "Expected 1 release in first batch") - assert.Equal(t, "721620118165", delivery.Batches[0].Releases[0].ReleaseID, "Release ID doesn't match expected") - assert.Equal(t, "20161024145603121/721620118165/721620118165.xml", delivery.Batches[0].Releases[0].XmlFilePath, "Release XML path doesn't match expected") - // Verify the parser (pending_deliveries collection) - doc, err = waitForDocument(e.Ctx, e.PendingReleasesColl, bson.M{"delivery_etag": eTag}) - if err != nil { - t.Fatalf("Error finding pending release in Mongo: %v", err) - } - if doc.Err() == mongo.ErrNoDocuments { - t.Fatalf("No pending release was found in Mongo: %v", doc.Err()) - } - var pendingRelease common.PendingRelease - if err = doc.Decode(&pendingRelease); err != nil { - t.Fatalf("Failed to decode pending release from Mongo: %v", err) + type subTest struct { + path string + eTag string + expectedD common.Delivery + expectedPR common.PendingRelease } - publishDate := time.Date(2010, time.October, 1, 0, 0, 0, 0, time.UTC) - assert.Equal(t, publishDate, pendingRelease.PublishDate, "publish_date doesn't match expected") - assert.Equal(t, common.CreateTrackRelease{}, pendingRelease.CreateTrackRelease, "Unexpected non-empty track release") - assert.Equal(t, []string(nil), delivery.ValidationErrors, "Expected there to be no validation errors") - assert.Equal(t, []string{}, delivery.Batches[0].ValidationErrors, "Expected there to be no validation errors") - assert.Equal(t, []string{}, pendingRelease.PublishErrors, "Expected there to be no publish errors") - assert.Equal(t, common.CreateAlbumRelease{ - Metadata: common.CollectionMetadata{ - PlaylistName: "A Monkey Claw in a Velvet Glove", - PlaylistOwnerID: "abcdef", - PlaylistOwnerName: "Monkey Claw", - IsAlbum: true, - IsPrivate: false, - Genre: "Metal", - ReleaseDate: publishDate, - DDEXReleaseIDs: common.ReleaseIDs{ - ICPN: "721620118165", - }, - CoverArtURL: "s3://audius-test-crawled/721620118165/resources/721620118165_T7_007.jpg", - Artists: []common.ResourceContributor{{ - Name: "Monkey Claw", - Roles: []string{"MainArtist"}, - SequenceNumber: 1, - }}, - CopyrightLine: &common.Copyright{ - Year: "2010", - Text: "(C) 2010 Iron Crown Music", - }, - ProducerCopyrightLine: &common.Copyright{ - Year: "2010", - Text: "(P) 2010 Iron Crown Music", - }, - ParentalWarningType: stringPtr("NotExplicit"), - }, - DDEXReleaseRef: "R0", - Tracks: []common.TrackMetadata{ - { - Title: "Can you feel ...the Monkey Claw!", - ReleaseDate: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), - DDEXReleaseIDs: common.ReleaseIDs{ - ISRC: "CASE00000001", - }, - Genre: "Metal", - Duration: 811, - ISRC: stringPtr("CASE00000001"), - Artists: []common.ResourceContributor{ + + releaseByReleaseTests := []subTest{ + { + path: "release_by_release/ern381/sony1.zip", + eTag: "bed7beaa33eed67bb1ba73353dd51e1d", + expectedD: common.Delivery{ + ZIPFilePath: "s3://audius-test-raw/sony1.zip", + ZIPFileETag: "bed7beaa33eed67bb1ba73353dd51e1d", + DeliveryStatus: constants.DeliveryStatusSuccess, + Batches: nil, + Releases: []common.UnprocessedRelease{ { - Name: "Monkey Claw", - Roles: []string{"MainArtist"}, - SequenceNumber: 1, + ReleaseID: "A10301A0005108088N", + XmlFilePath: "A10301A0005108088N/A10301A0005108088N.xml", + ValidationErrors: []string{}, }, }, - ResourceContributors: []common.ResourceContributor{{ - Name: "Steve Albino", - Roles: []string{"Producer"}, - SequenceNumber: 1, - }}, - IndirectResourceContributors: []common.ResourceContributor{{ - Name: "Bob Black", - Roles: []string{"Composer"}, - SequenceNumber: 1, - }}, - AudioFileURL: "s3://audius-test-crawled/721620118165/resources/721620118165_T1_001.wav", - CopyrightLine: &common.Copyright{ - Year: "2010", - Text: "(C) 2010 Iron Crown Music", - }, - ProducerCopyrightLine: &common.Copyright{ - Year: "2010", - Text: "(P) 2010 Iron Crown Music", - }, - ParentalWarningType: stringPtr("NotExplicit"), + ValidationErrors: []string(nil), }, - { - Title: "Red top mountain, blown sky high", - ReleaseDate: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), - DDEXReleaseIDs: common.ReleaseIDs{ - ISRC: "CASE00000002", - }, - Genre: "Metal", - Duration: 366, - ISRC: stringPtr("CASE00000002"), - Artists: []common.ResourceContributor{ - { - Name: "Monkey Claw", - Roles: []string{"MainArtist"}, - SequenceNumber: 1, + expectedPR: common.PendingRelease{ + ReleaseID: "A10301A0005108088N", + DeliveryETag: "bed7beaa33eed67bb1ba73353dd51e1d", + PublishDate: time.Date(2023, time.September, 1, 0, 0, 0, 0, time.UTC), + PublishErrors: []string{}, + CreateTrackRelease: common.CreateTrackRelease{}, + CreateAlbumRelease: common.CreateAlbumRelease{ + DDEXReleaseRef: "R0", + Metadata: common.CollectionMetadata{ + PlaylistName: "Present.", + PlaylistOwnerID: "Bmv3bJ", + PlaylistOwnerName: "Theo Random", + ReleaseDate: time.Date(2023, time.September, 1, 0, 0, 0, 0, time.UTC), + DDEXReleaseIDs: common.ReleaseIDs{ + CatalogNumber: "G010005108088N", + GRid: "A10301A0005108088N", + ICPN: "196871335584", + }, + IsAlbum: true, + IsPrivate: false, + Genre: common.HipHopRap, + CoverArtURL: "s3://audius-test-crawled/A10301A0005108088N/resources/A10301A0005108088N_T-1027024165547_Image.jpg", + CoverArtURLHash: "582fb410615167205e8741580cf77e71", + CoverArtURLHashAlgo: "MD5", + Artists: []common.ResourceContributor{{ + Name: "Theo Random", + Roles: []string{"MainArtist"}, + SequenceNumber: 1, + }}, + ProducerCopyrightLine: &common.Copyright{ + Year: "2023", + Text: "(P) 2023 South Africa - Sony Music Entertainment Africa (Pty) Ltd, under Sound African Recordings a division of Sony Music Entertainment Africa (Pty) Ltd", + }, + ParentalWarningType: stringPtr("Explicit"), }, - }, - ResourceContributors: []common.ResourceContributor{{ - Name: "Steve Albino", - Roles: []string{"Producer"}, - SequenceNumber: 1, - }}, - IndirectResourceContributors: []common.ResourceContributor{{ - Name: "Bob Black", - Roles: []string{"Composer"}, - SequenceNumber: 1, - }}, - AudioFileURL: "s3://audius-test-crawled/721620118165/resources/721620118165_T2_002.wav", - CopyrightLine: &common.Copyright{ - Year: "2010", - Text: "(C) 2010 Iron Crown Music", - }, - ProducerCopyrightLine: &common.Copyright{ - Year: "2010", - Text: "(P) 2010 Iron Crown Music", - }, - ParentalWarningType: stringPtr("NotExplicit"), - }, - { - Title: "Seige of Antioch", - ReleaseDate: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), - DDEXReleaseIDs: common.ReleaseIDs{ - ISRC: "CASE00000003", - }, - Genre: "Metal", - Duration: 1269, - ISRC: stringPtr("CASE00000003"), - Artists: []common.ResourceContributor{ - { - Name: "Monkey Claw", - Roles: []string{"MainArtist"}, - SequenceNumber: 1, + Tracks: []common.TrackMetadata{ + { + Title: "Playing With Fire.", + ReleaseDate: time.Time{}, + Genre: common.HipHopRap, + Duration: 279, + Artists: []common.ResourceContributor{{ + Name: "Theo Random", + Roles: []string{"AssociatedPerformer", "MainArtist"}, + SequenceNumber: 1, + }}, + ResourceContributors: []common.ResourceContributor{ + { + Name: "Thabiso Moya", + Roles: []string{"Composer", "Lyricist"}, + SequenceNumber: 1, + }, + { + Name: "Melange", + Roles: []string{"Producer"}, + SequenceNumber: 2, + }, + { + Name: "Neo Ndungane", + Roles: []string{"Composer", "Lyricist"}, + SequenceNumber: 3, + }, + { + Name: "Feziekk", + Roles: []string{"Producer"}, + SequenceNumber: 4, + }, + { + Name: "Regaugetsue Refenyeditswe Leshabane", + Roles: []string{"Composer", "Lyricist"}, + SequenceNumber: 5, + }, + { + Name: "Gabe Archibald Horowitz", + Roles: []string{"Composer", "Lyricist"}, + SequenceNumber: 6, + }, + }, + ArtistID: "", + ArtistName: "", + ISRC: stringPtr("ZAA012300131"), + DDEXReleaseIDs: common.ReleaseIDs{ + GRid: "A10328E0010879163O", + ISRC: "ZAA012300131", + }, + PreviewStartSeconds: intPtr(48), + PreviewAudioFileURL: "s3://audius-test-crawled/A10301A0005108088N/", + AudioFileURL: "s3://audius-test-crawled/A10301A0005108088N/resources/A10301A0005108088N_T-1096524256352_SoundRecording_001-001.mp3", + AudioFileURLHash: "8bb2ce119257314a8fcb215a49f14b33", + AudioFileURLHashAlgo: "MD5", + ProducerCopyrightLine: &common.Copyright{ + Year: "2023", + Text: "(P) 2023 South Africa - Sony Music Entertainment Africa (Pty) Ltd, under Sound African Recordings a division of Sony Music Entertainment Africa (Pty) Ltd", + }, + ParentalWarningType: stringPtr("Explicit"), + }, + { + Title: "No Comment.", + ReleaseDate: time.Time{}, + Genre: common.HipHopRap, + Duration: 142, + ArtistID: "", + ArtistName: "", + Artists: []common.ResourceContributor{ + { + Name: "Theo Random", + Roles: []string{"AssociatedPerformer", "MainArtist"}, + SequenceNumber: 1, + }, + { + Name: "Thato Saul", + Roles: []string{"AssociatedPerformer", "MainArtist"}, + SequenceNumber: 2, + }, + }, + ResourceContributors: []common.ResourceContributor{ + { + Name: "Theo Random & Thato Saul", + Roles: []string{"AssociatedPerformer"}, + SequenceNumber: 1, + }, + { + Name: "Thabiso Moya", + Roles: []string{"Composer", "Lyricist"}, + SequenceNumber: 2, + }, + { + Name: "Melange", + Roles: []string{"Producer"}, + SequenceNumber: 3, + }, + { + Name: "Thato Matlebyane", + Roles: []string{"Composer", "Lyricist"}, + SequenceNumber: 4, + }, + { + Name: "Neo Ndungane", + Roles: []string{"Composer", "Lyricist"}, + SequenceNumber: 5, + }, + }, + ISRC: stringPtr("ZAA012300128"), + DDEXReleaseIDs: common.ReleaseIDs{ + GRid: "A10328E0010879164M", + ISRC: "ZAA012300128", + }, + PreviewStartSeconds: intPtr(48), + PreviewAudioFileURL: "s3://audius-test-crawled/A10301A0005108088N/", + AudioFileURL: "s3://audius-test-crawled/A10301A0005108088N/resources/A10301A0005108088N_T-1096524142976_SoundRecording_001-002.mp3", + AudioFileURLHash: "1e9183898a4f6b45f895e45cd18ba162", + AudioFileURLHashAlgo: "MD5", + ProducerCopyrightLine: &common.Copyright{ + Year: "2023", + Text: "(P) 2023 South Africa - Sony Music Entertainment Africa (Pty) Ltd, under Sound African Recordings a division of Sony Music Entertainment Africa (Pty) Ltd", + }, + ParentalWarningType: stringPtr("Explicit"), + }, }, }, - ResourceContributors: []common.ResourceContributor{{ - Name: "Steve Albino", - Roles: []string{"Producer"}, - SequenceNumber: 1, - }}, - IndirectResourceContributors: []common.ResourceContributor{{ - Name: "Bob Black", - Roles: []string{"Composer"}, - SequenceNumber: 1, - }}, - AudioFileURL: "s3://audius-test-crawled/721620118165/resources/721620118165_T3_003.wav", - CopyrightLine: &common.Copyright{ - Year: "2010", - Text: "(C) 2010 Iron Crown Music", - }, - ProducerCopyrightLine: &common.Copyright{ - Year: "2010", - Text: "(P) 2010 Iron Crown Music", - }, - ParentalWarningType: stringPtr("NotExplicit"), }, - { - Title: "Warhammer", - ReleaseDate: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), - DDEXReleaseIDs: common.ReleaseIDs{ - ISRC: "CASE00000004", - }, - Genre: "Metal", - Duration: 165, - ISRC: stringPtr("CASE00000004"), - Artists: []common.ResourceContributor{ + }, + } + + batchedTests := []subTest{ + { + path: "batch/ern382/CPD1.zip", + eTag: "5e425b53234b868374c0b02e0b58b1cc", + expectedD: common.Delivery{ + ZIPFilePath: "s3://audius-test-raw/CPD1.zip", + ZIPFileETag: "5e425b53234b868374c0b02e0b58b1cc", + DeliveryStatus: constants.DeliveryStatusSuccess, + Releases: nil, + Batches: []common.UnprocessedBatch{ { - Name: "Monkey Claw", - Roles: []string{"MainArtist"}, - SequenceNumber: 1, + BatchID: "20161024145603121", + BatchXmlPath: "20161024145603121/BatchComplete_20161024145603121.xml", + Releases: []common.UnprocessedRelease{ + { + ReleaseID: "721620118165", + XmlFilePath: "20161024145603121/721620118165/721620118165.xml", + ValidationErrors: []string{}, + }, + }, + ValidationErrors: []string{}, }, }, - ResourceContributors: []common.ResourceContributor{{ - Name: "Steve Albino", - Roles: []string{"Producer"}, - SequenceNumber: 1, - }}, - IndirectResourceContributors: []common.ResourceContributor{{ - Name: "Bob Black", - Roles: []string{"Composer"}, - SequenceNumber: 1, - }}, - AudioFileURL: "s3://audius-test-crawled/721620118165/resources/721620118165_T4_004.wav", - CopyrightLine: &common.Copyright{ - Year: "2010", - Text: "(C) 2010 Iron Crown Music", - }, - ProducerCopyrightLine: &common.Copyright{ - Year: "2010", - Text: "(P) 2010 Iron Crown Music", - }, - ParentalWarningType: stringPtr("NotExplicit"), + ValidationErrors: []string(nil), }, - { - Title: "Iron Horse", - ReleaseDate: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), - DDEXReleaseIDs: common.ReleaseIDs{ - ISRC: "CASE00000005", - }, - Genre: "Metal", - Duration: 294, - ISRC: stringPtr("CASE00000005"), - Artists: []common.ResourceContributor{ - { - Name: "Monkey Claw", - Roles: []string{"MainArtist"}, - SequenceNumber: 1, + expectedPR: common.PendingRelease{ + ReleaseID: "721620118165", + DeliveryETag: "5e425b53234b868374c0b02e0b58b1cc", + PublishDate: time.Date(2010, time.October, 1, 0, 0, 0, 0, time.UTC), + PublishErrors: []string{}, + CreateTrackRelease: common.CreateTrackRelease{}, + CreateAlbumRelease: common.CreateAlbumRelease{ + Metadata: common.CollectionMetadata{ + PlaylistName: "A Monkey Claw in a Velvet Glove", + PlaylistOwnerID: "abcdef", + PlaylistOwnerName: "Monkey Claw", + IsAlbum: true, + IsPrivate: false, + Genre: "Metal", + ReleaseDate: time.Date(2010, time.October, 1, 0, 0, 0, 0, time.UTC), + DDEXReleaseIDs: common.ReleaseIDs{ + ICPN: "721620118165", + }, + CoverArtURL: "s3://audius-test-crawled/721620118165/resources/721620118165_T7_007.jpg", + Artists: []common.ResourceContributor{{ + Name: "Monkey Claw", + Roles: []string{"MainArtist"}, + SequenceNumber: 1, + }}, + CopyrightLine: &common.Copyright{ + Year: "2010", + Text: "(C) 2010 Iron Crown Music", + }, + ProducerCopyrightLine: &common.Copyright{ + Year: "2010", + Text: "(P) 2010 Iron Crown Music", + }, + ParentalWarningType: stringPtr("NotExplicit"), }, - }, - ResourceContributors: []common.ResourceContributor{{ - Name: "Steve Albino", - Roles: []string{"Producer"}, - SequenceNumber: 1, - }}, - IndirectResourceContributors: []common.ResourceContributor{{ - Name: "Bob Black", - Roles: []string{"Composer"}, - SequenceNumber: 1, - }}, - AudioFileURL: "s3://audius-test-crawled/721620118165/resources/721620118165_T5_005.wav", - CopyrightLine: &common.Copyright{ - Year: "2010", - Text: "(C) 2010 Iron Crown Music", - }, - ProducerCopyrightLine: &common.Copyright{ - Year: "2010", - Text: "(P) 2010 Iron Crown Music", - }, - ParentalWarningType: stringPtr("NotExplicit"), - }, - { - Title: "Yes... I can feel the Monkey Claw!", - ReleaseDate: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), - DDEXReleaseIDs: common.ReleaseIDs{ - ISRC: "CASE00000006", - }, - Genre: "Metal", - Duration: 741, - ISRC: stringPtr("CASE00000006"), - Artists: []common.ResourceContributor{ - { - Name: "Monkey Claw", - Roles: []string{"MainArtist"}, - SequenceNumber: 1, + DDEXReleaseRef: "R0", + Tracks: []common.TrackMetadata{ + { + Title: "Can you feel ...the Monkey Claw!", + ReleaseDate: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + DDEXReleaseIDs: common.ReleaseIDs{ + ISRC: "CASE00000001", + }, + Genre: "Metal", + Duration: 811, + ISRC: stringPtr("CASE00000001"), + Artists: []common.ResourceContributor{ + { + Name: "Monkey Claw", + Roles: []string{"MainArtist"}, + SequenceNumber: 1, + }, + }, + ResourceContributors: []common.ResourceContributor{{ + Name: "Steve Albino", + Roles: []string{"Producer"}, + SequenceNumber: 1, + }}, + IndirectResourceContributors: []common.ResourceContributor{{ + Name: "Bob Black", + Roles: []string{"Composer"}, + SequenceNumber: 1, + }}, + AudioFileURL: "s3://audius-test-crawled/721620118165/resources/721620118165_T1_001.wav", + CopyrightLine: &common.Copyright{ + Year: "2010", + Text: "(C) 2010 Iron Crown Music", + }, + ProducerCopyrightLine: &common.Copyright{ + Year: "2010", + Text: "(P) 2010 Iron Crown Music", + }, + ParentalWarningType: stringPtr("NotExplicit"), + }, + { + Title: "Red top mountain, blown sky high", + ReleaseDate: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + DDEXReleaseIDs: common.ReleaseIDs{ + ISRC: "CASE00000002", + }, + Genre: "Metal", + Duration: 366, + ISRC: stringPtr("CASE00000002"), + Artists: []common.ResourceContributor{ + { + Name: "Monkey Claw", + Roles: []string{"MainArtist"}, + SequenceNumber: 1, + }, + }, + ResourceContributors: []common.ResourceContributor{{ + Name: "Steve Albino", + Roles: []string{"Producer"}, + SequenceNumber: 1, + }}, + IndirectResourceContributors: []common.ResourceContributor{{ + Name: "Bob Black", + Roles: []string{"Composer"}, + SequenceNumber: 1, + }}, + AudioFileURL: "s3://audius-test-crawled/721620118165/resources/721620118165_T2_002.wav", + CopyrightLine: &common.Copyright{ + Year: "2010", + Text: "(C) 2010 Iron Crown Music", + }, + ProducerCopyrightLine: &common.Copyright{ + Year: "2010", + Text: "(P) 2010 Iron Crown Music", + }, + ParentalWarningType: stringPtr("NotExplicit"), + }, + { + Title: "Seige of Antioch", + ReleaseDate: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + DDEXReleaseIDs: common.ReleaseIDs{ + ISRC: "CASE00000003", + }, + Genre: "Metal", + Duration: 1269, + ISRC: stringPtr("CASE00000003"), + Artists: []common.ResourceContributor{ + { + Name: "Monkey Claw", + Roles: []string{"MainArtist"}, + SequenceNumber: 1, + }, + }, + ResourceContributors: []common.ResourceContributor{{ + Name: "Steve Albino", + Roles: []string{"Producer"}, + SequenceNumber: 1, + }}, + IndirectResourceContributors: []common.ResourceContributor{{ + Name: "Bob Black", + Roles: []string{"Composer"}, + SequenceNumber: 1, + }}, + AudioFileURL: "s3://audius-test-crawled/721620118165/resources/721620118165_T3_003.wav", + CopyrightLine: &common.Copyright{ + Year: "2010", + Text: "(C) 2010 Iron Crown Music", + }, + ProducerCopyrightLine: &common.Copyright{ + Year: "2010", + Text: "(P) 2010 Iron Crown Music", + }, + ParentalWarningType: stringPtr("NotExplicit"), + }, + { + Title: "Warhammer", + ReleaseDate: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + DDEXReleaseIDs: common.ReleaseIDs{ + ISRC: "CASE00000004", + }, + Genre: "Metal", + Duration: 165, + ISRC: stringPtr("CASE00000004"), + Artists: []common.ResourceContributor{ + { + Name: "Monkey Claw", + Roles: []string{"MainArtist"}, + SequenceNumber: 1, + }, + }, + ResourceContributors: []common.ResourceContributor{{ + Name: "Steve Albino", + Roles: []string{"Producer"}, + SequenceNumber: 1, + }}, + IndirectResourceContributors: []common.ResourceContributor{{ + Name: "Bob Black", + Roles: []string{"Composer"}, + SequenceNumber: 1, + }}, + AudioFileURL: "s3://audius-test-crawled/721620118165/resources/721620118165_T4_004.wav", + CopyrightLine: &common.Copyright{ + Year: "2010", + Text: "(C) 2010 Iron Crown Music", + }, + ProducerCopyrightLine: &common.Copyright{ + Year: "2010", + Text: "(P) 2010 Iron Crown Music", + }, + ParentalWarningType: stringPtr("NotExplicit"), + }, + { + Title: "Iron Horse", + ReleaseDate: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + DDEXReleaseIDs: common.ReleaseIDs{ + ISRC: "CASE00000005", + }, + Genre: "Metal", + Duration: 294, + ISRC: stringPtr("CASE00000005"), + Artists: []common.ResourceContributor{ + { + Name: "Monkey Claw", + Roles: []string{"MainArtist"}, + SequenceNumber: 1, + }, + }, + ResourceContributors: []common.ResourceContributor{{ + Name: "Steve Albino", + Roles: []string{"Producer"}, + SequenceNumber: 1, + }}, + IndirectResourceContributors: []common.ResourceContributor{{ + Name: "Bob Black", + Roles: []string{"Composer"}, + SequenceNumber: 1, + }}, + AudioFileURL: "s3://audius-test-crawled/721620118165/resources/721620118165_T5_005.wav", + CopyrightLine: &common.Copyright{ + Year: "2010", + Text: "(C) 2010 Iron Crown Music", + }, + ProducerCopyrightLine: &common.Copyright{ + Year: "2010", + Text: "(P) 2010 Iron Crown Music", + }, + ParentalWarningType: stringPtr("NotExplicit"), + }, + { + Title: "Yes... I can feel the Monkey Claw!", + ReleaseDate: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + DDEXReleaseIDs: common.ReleaseIDs{ + ISRC: "CASE00000006", + }, + Genre: "Metal", + Duration: 741, + ISRC: stringPtr("CASE00000006"), + Artists: []common.ResourceContributor{ + { + Name: "Monkey Claw", + Roles: []string{"MainArtist"}, + SequenceNumber: 1, + }, + }, + ResourceContributors: []common.ResourceContributor{{ + Name: "Steve Albino", + Roles: []string{"Producer"}, + SequenceNumber: 1, + }}, + IndirectResourceContributors: []common.ResourceContributor{{ + Name: "Bob Black", + Roles: []string{"Composer"}, + SequenceNumber: 1, + }}, + AudioFileURL: "s3://audius-test-crawled/721620118165/resources/721620118165_T6_006.wav", + CopyrightLine: &common.Copyright{ + Year: "2010", + Text: "(C) 2010 Iron Crown Music", + }, + ProducerCopyrightLine: &common.Copyright{ + Year: "2010", + Text: "(P) 2010 Iron Crown Music", + }, + ParentalWarningType: stringPtr("NotExplicit"), + }, }, }, - ResourceContributors: []common.ResourceContributor{{ - Name: "Steve Albino", - Roles: []string{"Producer"}, - SequenceNumber: 1, - }}, - IndirectResourceContributors: []common.ResourceContributor{{ - Name: "Bob Black", - Roles: []string{"Composer"}, - SequenceNumber: 1, - }}, - AudioFileURL: "s3://audius-test-crawled/721620118165/resources/721620118165_T6_006.wav", - CopyrightLine: &common.Copyright{ - Year: "2010", - Text: "(C) 2010 Iron Crown Music", - }, - ProducerCopyrightLine: &common.Copyright{ - Year: "2010", - Text: "(P) 2010 Iron Crown Music", - }, - ParentalWarningType: stringPtr("NotExplicit"), }, }, - }, pendingRelease.CreateAlbumRelease, "Album release doesn't match expected") -} - -func (e *e2eTest) runERN381ReleaseByRelease(t *testing.T) { - e.Logger.Info("Starting E2E test for ERN 381 Release-By-Release choreography") + } - e.uploadFixture(t, "release_by_release/ern381/sony1.zip") - eTag := "b0271b98d23e02947e86b5857e25e4c0" + // Run subtests for release-by-release or batched depending on env var - // Verify the crawler (deliveries collection) - doc, err := waitForDocument(e.Ctx, e.DeliveriesColl, bson.M{"_id": eTag}) - if err != nil { - t.Fatalf("Error finding delivery in Mongo: %v", err) - } - if doc.Err() == mongo.ErrNoDocuments { - t.Fatalf("No delivery was found in Mongo: %v", doc.Err()) - } - var delivery common.Delivery - if err = doc.Decode(&delivery); err != nil { - t.Fatalf("Failed to decode delivery from Mongo: %v", err) + var subTests []subTest + choreography := common.MustGetChoreography() + if choreography == constants.ERNReleaseByRelease { + subTests = releaseByReleaseTests + } else if choreography == constants.ERNBatched { + subTests = batchedTests + } else { + t.Fatalf("Unexpected choreography: %s", choreography) } - assert.Equal(t, "s3://audius-test-raw/sony1.zip", delivery.ZIPFilePath, "Path doesn't match expected") - assert.Equal(t, delivery.ZIPFileETag, eTag, "ETag (delivery ID) doesn't match expected") - assert.Equal(t, constants.DeliveryStatusParsing, delivery.DeliveryStatus, "delivery_status doesn't match expected") - assert.Equal(t, 1, len(delivery.Releases), "Expected 1 release in delivery") - assert.Equal(t, 0, len(delivery.Batches), "Expected 0 batches in delivery") - assert.Equal(t, "A10301A0005108088N", delivery.Releases[0].ReleaseID, "Release ID doesn't match expected") - assert.Equal(t, "A10301A0005108088N/A10301A0005108088N.xml", delivery.Releases[0].XmlFilePath, "XML path doesn't match expected") - // Verify the parser (pending_deliveries collection) - doc, err = waitForDocument(e.Ctx, e.PendingReleasesColl, bson.M{"delivery_etag": eTag}) - if err != nil { - t.Fatalf("Error finding pending release in Mongo: %v", err) - } - if doc.Err() == mongo.ErrNoDocuments { - t.Fatalf("No pending release was found in Mongo: %v", doc.Err()) - } - var pendingRelease common.PendingRelease - if err = doc.Decode(&pendingRelease); err != nil { - t.Fatalf("Failed to decode pending release from Mongo: %v", err) - } - publishDate := time.Date(2023, time.September, 1, 0, 0, 0, 0, time.UTC) - assert.Equal(t, publishDate, pendingRelease.PublishDate, "publish_date doesn't match expected") - assert.Equal(t, common.CreateTrackRelease{}, pendingRelease.CreateTrackRelease, "Unexpected non-empty track release") - assert.Equal(t, []string(nil), delivery.ValidationErrors, "Expected there to be no validation errors") - assert.Equal(t, []string{}, delivery.Releases[0].ValidationErrors, "Expected there to be no validation errors") - assert.Equal(t, []string{}, pendingRelease.PublishErrors, "Expected there to be no publish errors") - assert.Equal(t, common.CreateAlbumRelease{ - DDEXReleaseRef: "R0", - Metadata: common.CollectionMetadata{ - PlaylistName: "Present.", - PlaylistOwnerID: "Bmv3bJ", - PlaylistOwnerName: "Theo Random", - ReleaseDate: publishDate, - DDEXReleaseIDs: common.ReleaseIDs{ - CatalogNumber: "G010005108088N", - GRid: "A10301A0005108088N", - ICPN: "196871335584", - }, - IsAlbum: true, - IsPrivate: false, - Genre: common.HipHopRap, - CoverArtURL: fmt.Sprintf("s3://audius-test-crawled/%s/resources/A10301A0005108088N_T-1027024165547_Image.jpg", pendingRelease.ReleaseID), - CoverArtURLHash: "582fb410615167205e8741580cf77e71", - CoverArtURLHashAlgo: "MD5", - Artists: []common.ResourceContributor{{ - Name: "Theo Random", - Roles: []string{"MainArtist"}, - SequenceNumber: 1, - }}, - ProducerCopyrightLine: &common.Copyright{ - Year: "2023", - Text: "(P) 2023 South Africa - Sony Music Entertainment Africa (Pty) Ltd, under Sound African Recordings a division of Sony Music Entertainment Africa (Pty) Ltd", - }, - ParentalWarningType: stringPtr("Explicit"), - }, - Tracks: []common.TrackMetadata{ - { - Title: "Playing With Fire.", - ReleaseDate: time.Time{}, - Genre: common.HipHopRap, - Duration: 279, - Artists: []common.ResourceContributor{{ - Name: "Theo Random", - Roles: []string{"AssociatedPerformer", "MainArtist"}, - SequenceNumber: 1, - }}, - ResourceContributors: []common.ResourceContributor{ - { - Name: "Thabiso Moya", - Roles: []string{"Composer", "Lyricist"}, - SequenceNumber: 1, - }, - { - Name: "Melange", - Roles: []string{"Producer"}, - SequenceNumber: 2, - }, - { - Name: "Neo Ndungane", - Roles: []string{"Composer", "Lyricist"}, - SequenceNumber: 3, - }, - { - Name: "Feziekk", - Roles: []string{"Producer"}, - SequenceNumber: 4, - }, - { - Name: "Regaugetsue Refenyeditswe Leshabane", - Roles: []string{"Composer", "Lyricist"}, - SequenceNumber: 5, - }, - { - Name: "Gabe Archibald Horowitz", - Roles: []string{"Composer", "Lyricist"}, - SequenceNumber: 6, - }, - }, - ArtistID: "", - ArtistName: "", - ISRC: stringPtr("ZAA012300131"), - DDEXReleaseIDs: common.ReleaseIDs{ - GRid: "A10328E0010879163O", - ISRC: "ZAA012300131", - }, - PreviewStartSeconds: intPtr(48), - PreviewAudioFileURL: fmt.Sprintf("s3://audius-test-crawled/%s/", pendingRelease.ReleaseID), - AudioFileURL: fmt.Sprintf("s3://audius-test-crawled/%s/resources/A10301A0005108088N_T-1096524256352_SoundRecording_001-001.m4a", pendingRelease.ReleaseID), - AudioFileURLHash: "8bb2ce119257314a8fcb215a49f14b33", - AudioFileURLHashAlgo: "MD5", - ProducerCopyrightLine: &common.Copyright{ - Year: "2023", - Text: "(P) 2023 South Africa - Sony Music Entertainment Africa (Pty) Ltd, under Sound African Recordings a division of Sony Music Entertainment Africa (Pty) Ltd", - }, - ParentalWarningType: stringPtr("Explicit"), - }, - { - Title: "No Comment.", - ReleaseDate: time.Time{}, - Genre: common.HipHopRap, - Duration: 142, - ArtistID: "", - ArtistName: "", - Artists: []common.ResourceContributor{ - { - Name: "Theo Random", - Roles: []string{"AssociatedPerformer", "MainArtist"}, - SequenceNumber: 1, - }, - { - Name: "Thato Saul", - Roles: []string{"AssociatedPerformer", "MainArtist"}, - SequenceNumber: 2, - }, - }, - ResourceContributors: []common.ResourceContributor{ - { - Name: "Theo Random & Thato Saul", - Roles: []string{"AssociatedPerformer"}, - SequenceNumber: 1, - }, - { - Name: "Thabiso Moya", - Roles: []string{"Composer", "Lyricist"}, - SequenceNumber: 2, - }, - { - Name: "Melange", - Roles: []string{"Producer"}, - SequenceNumber: 3, - }, - { - Name: "Thato Matlebyane", - Roles: []string{"Composer", "Lyricist"}, - SequenceNumber: 4, - }, - { - Name: "Neo Ndungane", - Roles: []string{"Composer", "Lyricist"}, - SequenceNumber: 5, - }, - }, - ISRC: stringPtr("ZAA012300128"), - DDEXReleaseIDs: common.ReleaseIDs{ - GRid: "A10328E0010879164M", - ISRC: "ZAA012300128", - }, - PreviewStartSeconds: intPtr(48), - PreviewAudioFileURL: fmt.Sprintf("s3://audius-test-crawled/%s/", pendingRelease.ReleaseID), - AudioFileURL: fmt.Sprintf("s3://audius-test-crawled/%s/resources/A10301A0005108088N_T-1096524142976_SoundRecording_001-002.m4a", pendingRelease.ReleaseID), - AudioFileURLHash: "1e9183898a4f6b45f895e45cd18ba162", - AudioFileURLHashAlgo: "MD5", - ProducerCopyrightLine: &common.Copyright{ - Year: "2023", - Text: "(P) 2023 South Africa - Sony Music Entertainment Africa (Pty) Ltd, under Sound African Recordings a division of Sony Music Entertainment Africa (Pty) Ltd", - }, - ParentalWarningType: stringPtr("Explicit"), - }, - }, - }, pendingRelease.CreateAlbumRelease, "Album release doesn't match expected") + for _, st := range subTests { + uploadFixture(t, bi, st.path) + + // Verify the parser (pending_releases collection) + doc, err := waitForDocument(bi.Ctx, bi.PendingReleasesColl, bson.M{"delivery_etag": st.eTag}) + if err != nil { + t.Fatalf("Error finding pending release in Mongo: %v", err) + } + if doc.Err() == mongo.ErrNoDocuments { + t.Fatalf("No pending release was found in Mongo: %v", doc.Err()) + } + var pendingRelease common.PendingRelease + if err = doc.Decode(&pendingRelease); err != nil { + t.Fatalf("Failed to decode pending release from Mongo: %v", err) + } + + // Ignore CreatedAt because we can't predict the exact time of a Mongo insert + pendingRelease.CreatedAt = time.Time{} + + assert.Equal(t, st.expectedPR, pendingRelease) + + // Verify the crawler (deliveries collection) + doc, err = waitForDocument(bi.Ctx, bi.DeliveriesColl, bson.M{"_id": st.eTag}) + if err != nil { + t.Fatalf("Error finding delivery in Mongo: %v", err) + } + if doc.Err() == mongo.ErrNoDocuments { + t.Fatalf("No delivery was found in Mongo: %v", doc.Err()) + } + var delivery common.Delivery + if err = doc.Decode(&delivery); err != nil { + t.Fatalf("Failed to decode delivery from Mongo: %v", err) + } - // TODO: Leaving the publisher untested for now + // Ignore CreatedAt because we can't predict the exact time of a Mongo insert + delivery.CreatedAt = time.Time{} + + // Similarly, gnore XMLContent because it's too much to paste + for i := 0; i < len(delivery.Releases); i++ { + delivery.Releases[i].XmlContent = primitive.Binary{} + if len(st.expectedD.Releases) > i { + st.expectedD.Releases[i].XmlContent = primitive.Binary{} + } + } + for i := 0; i < len(delivery.Batches); i++ { + delivery.Batches[i].BatchXmlContent = primitive.Binary{} + if len(st.expectedD.Batches) > i { + st.expectedD.Batches[i].BatchXmlContent = primitive.Binary{} + } + for j := 0; j < len(delivery.Batches[i].Releases); j++ { + delivery.Batches[i].Releases[j].XmlContent = primitive.Binary{} + if len(st.expectedD.Batches) > i && len(st.expectedD.Batches[i].Releases) > j { + st.expectedD.Batches[i].Releases[j].XmlContent = primitive.Binary{} + } + } + } + assert.Equal(t, st.expectedD, delivery) + + // TODO: Leaving the publisher untested for now + } } func createBucket(s3Client *s3.S3, bucket string) error { @@ -614,16 +646,16 @@ func createBucket(s3Client *s3.S3, bucket string) error { } // uploadFixture uploads a test fixture to the S3 "raw" bucket -func (e *e2eTest) uploadFixture(t *testing.T, filepath string) { +func uploadFixture(t *testing.T, bi *common.BaseIngester, filepath string) { filepath = fmt.Sprintf("./fixtures/%s", filepath) file, err := os.Open(filepath) if err != nil { - e.Logger.Error("Failed to open test file", "error", err) + bi.Logger.Error("Failed to open test file", "error", err) } defer file.Close() - _, err = e.S3Uploader.Upload(&s3manager.UploadInput{ - Bucket: aws.String(e.RawBucket), + _, err = bi.S3Uploader.Upload(&s3manager.UploadInput{ + Bucket: aws.String(bi.RawBucket), Key: aws.String(strings.Split(filepath, "/")[len(strings.Split(filepath, "/"))-1]), Body: file, }) diff --git a/packages/ddex/ingester/e2e_test/fixtures/release_by_release/ern381/sony1.zip b/packages/ddex/ingester/e2e_test/fixtures/release_by_release/ern381/sony1.zip index 2176d1b16a4..44d16e00583 100644 Binary files a/packages/ddex/ingester/e2e_test/fixtures/release_by_release/ern381/sony1.zip and b/packages/ddex/ingester/e2e_test/fixtures/release_by_release/ern381/sony1.zip differ diff --git a/packages/ddex/ingester/parser/ern38x.go b/packages/ddex/ingester/parser/ern38x.go index 5dfaeb6f6a6..9721ccebc88 100644 --- a/packages/ddex/ingester/parser/ern38x.go +++ b/packages/ddex/ingester/parser/ern38x.go @@ -29,7 +29,7 @@ type SoundRecording struct { ProducerCopyrightLine *common.Copyright ParentalWarningType string LabelName string - Genre string + Genre common.Genre TechnicalDetails []TechnicalSoundRecordingDetails } @@ -232,7 +232,6 @@ func processReleaseNode(rNode *xmlquery.Node, soundRecordings *[]SoundRecording, title := safeInnerText(releaseDetails.SelectElement("Title[@TitleType='DisplayTitle']/TitleText")) // TODO: This assumes there aren't multiple titles in different languages (ie, different `LanguageAndScriptCode` attributes) artistName := safeInnerText(releaseDetails.SelectElement("DisplayArtistName")) - genreStr := safeInnerText(releaseDetails.SelectElement("Genre/GenreText")) parentalWarning := safeInnerText(releaseDetails.SelectElement("ParentalWarningType")) // Parse DisplayArtist nodes @@ -289,10 +288,12 @@ func processReleaseNode(rNode *xmlquery.Node, soundRecordings *[]SoundRecording, } } + genre, genreStrs := getGenres(releaseDetails) + if releaseType == "Album" { - genre, ok := common.ToGenre(genreStr) - if !ok { - err = fmt.Errorf("unsupported genre %s for %s", genreStr, releaseRef) + // Album is required to have a genre in its Release (not just a genre per track) + if genre == "" { + err = fmt.Errorf("no genre match in list '%v' for %s", genreStrs, releaseRef) return } @@ -411,10 +412,10 @@ func processReleaseNode(rNode *xmlquery.Node, soundRecordings *[]SoundRecording, } } + // Track could have a genre in its SoundRecording. If not, fall back to the genre in its Release element if trackMetadata.Genre == "" { - genre, ok := common.ToGenre(genreStr) - if !ok { - err = fmt.Errorf("unsupported genre %s for %s", genreStr, releaseRef) + if genre == "" { + err = fmt.Errorf("no genre match in list '%v' for %s", genreStrs, releaseRef) return } trackMetadata.Genre = genre @@ -456,7 +457,7 @@ func processReleaseNode(rNode *xmlquery.Node, soundRecordings *[]SoundRecording, return } -// parseTrackMetadata parses the metadata for a sound recording from a ResourceGroupContentItem. +// parseTrackMetadata parses the metadata for a sound recording from a ResourceGroupContentItem func parseTrackMetadata(ci ResourceGroupContentItem, crawledBucket, releaseID string) (metadata *common.TrackMetadata, err error) { if ci.SoundRecording == nil { err = fmt.Errorf("no found for %s", ci.Reference) @@ -472,6 +473,7 @@ func parseTrackMetadata(ci ResourceGroupContentItem, crawledBucket, releaseID st Title: ci.SoundRecording.Title, Duration: int(duration.Seconds()), ISRC: &ci.SoundRecording.ISRC, + Genre: ci.SoundRecording.Genre, Artists: ci.SoundRecording.Artists, ResourceContributors: ci.SoundRecording.ResourceContributors, IndirectResourceContributors: ci.SoundRecording.IndirectResourceContributors, @@ -501,10 +503,6 @@ func parseTrackMetadata(ci ResourceGroupContentItem, crawledBucket, releaseID st } } - if genre, ok := common.ToGenre(ci.SoundRecording.Genre); ok { - metadata.Genre = genre - } - if ci.SoundRecording.ParentalWarningType != "" { metadata.ParentalWarningType = &ci.SoundRecording.ParentalWarningType } @@ -568,6 +566,11 @@ func processSoundRecordingNode(sNode *xmlquery.Node) (recording *SoundRecording, return } + genre, genreStrs := getGenres(details) + if genre == "" { + fmt.Printf("no genre match in list '%v' for %s\n", genreStrs, resourceRef) + } + recording = &SoundRecording{ Type: safeInnerText(sNode.SelectElement("SoundRecordingType")), ISRC: safeInnerText(sNode.SelectElement("SoundRecordingId/ISRC")), @@ -577,7 +580,7 @@ func processSoundRecordingNode(sNode *xmlquery.Node) (recording *SoundRecording, LanguageOfPerformance: safeInnerText(sNode.SelectElement("LanguageOfPerformance")), Duration: safeInnerText(sNode.SelectElement("Duration")), LabelName: safeInnerText(details.SelectElement("LabelName")), - Genre: safeInnerText(details.SelectElement("Genre/GenreText")), + Genre: genre, ParentalWarningType: safeInnerText(details.SelectElement("ParentalWarningType")), } @@ -818,3 +821,24 @@ func parseISODuration(isoDuration string) (time.Duration, error) { duration := time.Duration(totalSeconds) * time.Second return duration, nil } + +// getGenres returns the first match of Genre for all SubGenre and GenreText elements, along with the ordered slice of strings it tried to match +func getGenres(node *xmlquery.Node) (genre common.Genre, genreStrs []string) { + for _, genreNode := range xmlquery.Find(node, "Genre") { + genreStrs = append(genreStrs, safeInnerText(genreNode.SelectElement("SubGenre"))) + } + for _, genreNode := range xmlquery.Find(node, "Genre") { + genreStrs = append(genreStrs, safeInnerText(genreNode.SelectElement("GenreText"))) + } + + var ok bool + for _, genreStr := range genreStrs { + genre, ok = common.ToGenre(genreStr) + if ok { + return + } else { + fmt.Printf("Skipping unsupported genre '%s'\n", genreStr) + } + } + return +} diff --git a/packages/ddex/ingester/parser/parser.go b/packages/ddex/ingester/parser/parser.go index 755aff309e9..b185888350f 100644 --- a/packages/ddex/ingester/parser/parser.go +++ b/packages/ddex/ingester/parser/parser.go @@ -95,7 +95,7 @@ func (p *Parser) processDelivery(changeStream *mongo.ChangeStream) { // Create a PendingRelease doc for each parsed release for _, pendingRelease := range pendingReleases { - result, err := p.PendingReleasesColl.InsertOne(p.Ctx, pendingRelease) + result, err := p.PendingReleasesColl.InsertOne(sessionCtx, pendingRelease) if err != nil { session.AbortTransaction(sessionCtx) return err @@ -103,7 +103,7 @@ func (p *Parser) processDelivery(changeStream *mongo.ChangeStream) { p.Logger.Info("Inserted pending release", "_id", result.InsertedID) } - delivery.DeliveryStatus = constants.DeliveryStatusSuccess + p.DeliveriesColl.UpdateByID(sessionCtx, delivery.ZIPFileETag, bson.M{"$set": bson.M{"delivery_status": constants.DeliveryStatusSuccess}}) return session.CommitTransaction(sessionCtx) }) diff --git a/packages/ddex/publisher/README.md b/packages/ddex/publisher/README.md index 54a3d487b16..d7715aefd66 100644 --- a/packages/ddex/publisher/README.md +++ b/packages/ddex/publisher/README.md @@ -3,10 +3,8 @@ Server that publishes DDEX entities queued for release by the DDEX ingester. ### Local Dev -Setup: -Make sure you've configured your `packages/ddex/.env` and S3 buckets according to the toplevel DDEX README. +The easiest way to test DDEX locally is via `audius-compose up --ddex-[release-by-release|batched]`. If you want to enable hot reloading for the publisher: -Run the server: -1. Make sure you can connect to mongo at `mongodb://mongo:mongo@localhost:27017/ddex`. See `packages/ddex/README.md` on how to spin up `ddex-mongo` and the other ddex containers. -2. At the monorepo root: `npm i` +1. Make sure the DDEX stack is running. See `packages/ddex/README.md` for instructions on how to bring up the DDEX stack locally. +2. `docker stop ddex-publisher` (assuming it's running as part of the whole DDEX stack) 3. At `packages/ddex/publisher`: `npm run dev:[dev|stage|prod]` diff --git a/packages/ddex/publisher/package.json b/packages/ddex/publisher/package.json index 1518b7d56d4..8922b76a8cd 100644 --- a/packages/ddex/publisher/package.json +++ b/packages/ddex/publisher/package.json @@ -1,6 +1,6 @@ { "name": "@audius/ddex-publisher", - "version": "0.0.9", + "version": "0.0.10", "description": "Server that publishes DDEX entities queued for release", "main": "index.js", "scripts": { diff --git a/packages/ddex/publisher/src/models/deliveries.ts b/packages/ddex/publisher/src/models/deliveries.ts index e2259e2111a..3579a1d2403 100644 --- a/packages/ddex/publisher/src/models/deliveries.ts +++ b/packages/ddex/publisher/src/models/deliveries.ts @@ -1,14 +1,31 @@ import mongoose from 'mongoose' -// DDEX deliveries indexed from DDEX uploads +const releaseSchema = new mongoose.Schema({ + release_id: String, + xml_file_path: String, + xml_content: Buffer, + validation_errors: [String], +}) + +const batchSchema = new mongoose.Schema({ + batch_id: String, + batch_xml_path: String, + batch_xml_content: Buffer, + validation_errors: [String], + releases: [releaseSchema], + ddex_schema: String, + num_messages: Number, +}) + +// DDEX deliveries crawled from the S3 "raw" bucket const deliveriesSchema = new mongoose.Schema({ _id: mongoose.Schema.Types.ObjectId, - upload_etag: String, + zip_file_path: String, delivery_status: String, - xml_file_path: String, - xml_content: Buffer, created_at: Date, - errors: [String], + releases: [releaseSchema], + batches: [batchSchema], + validation_errors: [String], }) const Deliveries = mongoose.model('Deliveries', deliveriesSchema, 'deliveries') diff --git a/packages/ddex/publisher/src/models/pendingReleases.ts b/packages/ddex/publisher/src/models/pendingReleases.ts index 89019f7b1b0..b29b9e9c9ae 100644 --- a/packages/ddex/publisher/src/models/pendingReleases.ts +++ b/packages/ddex/publisher/src/models/pendingReleases.ts @@ -198,18 +198,18 @@ export type CreateAlbumRelease = mongoose.InferSchemaType< > export const pendingReleasesSchema = new mongoose.Schema({ - upload_etag: { type: String, required: true }, - delivery_id: { type: mongoose.Schema.Types.ObjectId, required: true }, + _id: { type: String, required: true }, + delivery_etag: { type: String, required: true }, publish_date: { type: Date, required: true }, created_at: { type: Date, required: true }, create_track_release: createTrackReleaseSchema, create_album_release: createAlbumReleaseSchema, - upload_errors: [String], + publish_errors: [String], failure_count: Number, failed_after_upload: Boolean, }) -// Releases parsed from indexed DDEX deliveries that are awaiting publishing +// Releases awaiting publishing. Releases are parsed from DDEX deliveries const PendingReleases = mongoose.model( 'PendingReleases', pendingReleasesSchema, diff --git a/packages/ddex/publisher/src/models/publishedReleases.ts b/packages/ddex/publisher/src/models/publishedReleases.ts index 7da19c0d31d..a125e5d11a0 100644 --- a/packages/ddex/publisher/src/models/publishedReleases.ts +++ b/packages/ddex/publisher/src/models/publishedReleases.ts @@ -6,8 +6,8 @@ import { // DDEX releases that have been published const publishedReleasesSchema = new mongoose.Schema({ - upload_etag: String, - delivery_id: mongoose.Schema.Types.ObjectId, + _id: { type: String, required: true }, + delivery_etag: { type: String, required: true }, publish_date: Date, entity_id: String, blockhash: String, diff --git a/packages/ddex/publisher/src/services/publisherService.ts b/packages/ddex/publisher/src/services/publisherService.ts index 386204b7a3d..bd98777095f 100644 --- a/packages/ddex/publisher/src/services/publisherService.ts +++ b/packages/ddex/publisher/src/services/publisherService.ts @@ -1,9 +1,6 @@ import mongoose from 'mongoose' -import Deliveries from '../models/deliveries' import PendingReleases from '../models/pendingReleases' -import PublishedReleases, { - PublishedRelease, -} from '../models/publishedReleases' +import PublishedReleases from '../models/publishedReleases' import type { TrackMetadata, CollectionMetadata, @@ -37,7 +34,12 @@ const formatTrackMetadata = ( artists: metadata.artists, resourceContributors: metadata.resource_contributors, indirectResourceContributors: metadata.indirect_resource_contributors, - rightsController: metadata.rights_controller, + rightsController: metadata.rights_controller?.name + ? { + ...metadata.rights_controller, + roles: metadata.rights_controller?.roles ?? [], + } + : null, copyrightLine: metadata.copyright_line, producerCopyrightLine: metadata.producer_copyright_line, parentalWarningType: metadata.parental_warning_type, @@ -62,7 +64,7 @@ const formatAlbumMetadata = ( albumName: metadata.playlist_name, description: metadata.description || '', license: metadata.license || '', - mood: (metadata.mood || 'Other') as Mood, // TODO: SDK requires mood, but XML doesn't provide one + mood: (metadata.mood || 'Other') as Mood, // SDK requires mood, but XML doesn't provide one releaseDate: new Date(metadata.release_date), ddexReleaseIds: metadata.ddex_release_ids, tags: metadata.tags || '', @@ -82,7 +84,7 @@ const uploadTrack = async ( const userId = pendingTrack.metadata.artist_id const metadata = formatTrackMetadata(pendingTrack.metadata) - const coverArtDownload = await s3Service.downloadFromS3Indexed( + const coverArtDownload = await s3Service.downloadFromS3Crawled( pendingTrack.metadata.cover_art_url ) // TODO: We can hash and verify against the metadata here @@ -90,7 +92,7 @@ const uploadTrack = async ( buffer: coverArtDownload!, originalname: pendingTrack.metadata.cover_art_url.split('/').pop(), } - const trackDownload = await s3Service.downloadFromS3Indexed( + const trackDownload = await s3Service.downloadFromS3Crawled( pendingTrack.metadata.audio_file_url ) const trackFile = { @@ -119,7 +121,7 @@ const uploadAlbum = async ( s3Service: ReturnType ) => { // Fetch cover art from S3 - const coverArtDownload = await s3Service.downloadFromS3Indexed( + const coverArtDownload = await s3Service.downloadFromS3Crawled( pendingAlbum.metadata.cover_art_url ) // TODO: We can hash and verify against the metadata here @@ -130,7 +132,7 @@ const uploadAlbum = async ( // Fetch track audio files from S3 const trackFilesPromises = pendingAlbum.tracks.map(async (track) => { - const trackDownload = await s3Service.downloadFromS3Indexed( + const trackDownload = await s3Service.downloadFromS3Crawled( track.audio_file_url ) // TODO: We can hash and verify against the metadata here @@ -168,18 +170,19 @@ async function recordPendingReleaseErr( ) { let errorMsg = '' + console.error(error) + if (error instanceof Error) { errorMsg = error.message } else { errorMsg = 'An unknown error occurred' } - console.error(errorMsg) try { await PendingReleases.updateOne( { _id: doc._id }, { - $push: { upload_errors: errorMsg }, + $push: { publish_errors: errorMsg }, $inc: { failure_count: 1 }, $set: { failed_after_upload: failedAfterUpload }, } @@ -203,7 +206,7 @@ export const publishReleases = async ( const currentDate = new Date() documents = await PendingReleases.find({ publish_date: { $lte: currentDate }, - }) + }).lean() } catch (error) { console.error('Failed to fetch pending releases:', error) await new Promise((resolve) => setTimeout(resolve, 10_000)) @@ -211,41 +214,34 @@ export const publishReleases = async ( } for (const doc of documents) { + const releaseId = doc._id + if (doc.failed_after_upload) { console.error( - `pending_releases doc with delivery_id ${doc.delivery_id} requires manual intervention because it's already uploaded to Audius but failed to move to published_releases.` + `pending_releases doc with ID ${releaseId} requires manual intervention because it's already uploaded to Audius but failed to move to published_releases.` ) continue } - const deliveryId = doc.delivery_id - let publishedData: PublishedRelease - + let uploadResult: { + trackId?: string | null + albumId?: string | null + blockHash: string + blockNumber: number + } try { - if (doc.create_track_release) { - const uploadResult = await uploadTrack( + if (doc.create_track_release?.ddex_release_ref) { + uploadResult = await uploadTrack( audiusSdk, doc.create_track_release, s3 ) - publishedData = { - track: doc.create_track_release, - entity_id: uploadResult.trackId, - blockhash: uploadResult.blockHash, - blocknumber: uploadResult.blockNumber, - } - } else if (doc.create_album_release) { - const uploadResult = await uploadAlbum( + } else if (doc.create_album_release?.ddex_release_ref) { + uploadResult = await uploadAlbum( audiusSdk, doc.create_album_release, s3 ) - publishedData = { - album: doc.create_album_release, - entity_id: uploadResult.albumId, - blockhash: uploadResult.blockHash, - blocknumber: uploadResult.blockNumber, - } } else { recordPendingReleaseErr( doc, @@ -258,11 +254,17 @@ export const publishReleases = async ( continue } - publishedData = { - ...publishedData, + const publishedData = { + _id: releaseId, + track: doc.create_track_release, + album: doc.create_album_release, + entity_id: doc.create_track_release + ? uploadResult.trackId + : uploadResult.albumId, + blockhash: uploadResult.blockHash, + blocknumber: uploadResult.blockNumber, publish_date: doc.publish_date, - upload_etag: doc.upload_etag, - delivery_id: deliveryId, + delivery_etag: doc.delivery_etag, created_at: new Date(), } console.log('Published release: ', JSON.stringify(publishedData)) @@ -273,19 +275,8 @@ export const publishReleases = async ( session.startTransaction() const publishedRelease = new PublishedReleases(publishedData) await publishedRelease.save({ session }) - await PendingReleases.deleteOne({ _id: doc._id }).session(session) + await PendingReleases.deleteOne({ _id: releaseId }).session(session) - // Update delivery_status to 'published' once all releases in the delivery are published - const remainingCount = await PendingReleases.countDocuments({ - delivery_id: deliveryId, - }).session(session) - if (remainingCount === 0) { - await Deliveries.updateOne( - { _id: deliveryId }, - { $set: { delivery_status: 'published' } }, - { session } - ) - } await session.commitTransaction() } catch (error) { await session.abortTransaction() diff --git a/packages/ddex/publisher/src/services/s3Service.ts b/packages/ddex/publisher/src/services/s3Service.ts index aee7d92a4f3..a0f81ab3540 100644 --- a/packages/ddex/publisher/src/services/s3Service.ts +++ b/packages/ddex/publisher/src/services/s3Service.ts @@ -16,18 +16,18 @@ export default function createS3() { config.endpoint = process.env.AWS_ENDPOINT config.forcePathStyle = true } - const indexedBucket = process.env.AWS_BUCKET_INDEXED + const crawledBucket = process.env.AWS_BUCKET_CRAWLED const client = new S3Client(config) - const downloadFromS3Indexed = async (key: string) => { - if (key.startsWith(`s3://${indexedBucket}/`)) { - key = key.slice(`s3://${indexedBucket}/`.length) + const downloadFromS3Crawled = async (key: string) => { + if (key.startsWith(`s3://${crawledBucket}/`)) { + key = key.slice(`s3://${crawledBucket}/`.length) } else { - throw new Error(`Invalid key ${key} (not in indexed bucket)`) + throw new Error(`Invalid key '${key}' (not in crawled bucket)`) } const command = new GetObjectCommand({ - Bucket: indexedBucket, + Bucket: crawledBucket, Key: key, }) try { @@ -41,5 +41,5 @@ export default function createS3() { } } - return { downloadFromS3Indexed } + return { downloadFromS3Crawled } } diff --git a/packages/ddex/webapp/README.md b/packages/ddex/webapp/README.md index bb04f5c6790..d64b81fcaf0 100644 --- a/packages/ddex/webapp/README.md +++ b/packages/ddex/webapp/README.md @@ -7,7 +7,7 @@ The easiest way to test DDEX locally is via `audius-compose up --ddex-[release-b 1. Make sure the DDEX stack is running. See `packages/ddex/README.md` for instructions on how to bring up the DDEX stack locally. 2. `cp .env.dev .env` and comment/uncomment the env vars for either release-by-release or batched choreography -2. `docker stop ddex-web-release-by-release ddex-web-batched` (assuming it's running as part of the whole DDEX stack) +2. `docker stop ddex-web` (assuming it's running as part of the whole DDEX stack) 3. At `packages/ddex/webapp/server`: `npm run dev:[dev|stage|prod]` 4. At `packages/ddex/webapp/client`: `npm run start:[dev|stage|prod]` 5. You can now navigate to `http://localhost:5173` to view the DDEX webapp diff --git a/packages/ddex/webapp/client/package.json b/packages/ddex/webapp/client/package.json index dbe915257e2..60cba9cdd77 100644 --- a/packages/ddex/webapp/client/package.json +++ b/packages/ddex/webapp/client/package.json @@ -1,7 +1,7 @@ { "name": "@audius/ddex-webapp-client", "private": true, - "version": "0.0.16", + "version": "0.0.17", "type": "module", "scripts": { "start": "vite", diff --git a/packages/ddex/webapp/client/src/components/Collection/Collection.tsx b/packages/ddex/webapp/client/src/components/Collection/Collection.tsx index 5a16468a736..3c4c6a38234 100644 --- a/packages/ddex/webapp/client/src/components/Collection/Collection.tsx +++ b/packages/ddex/webapp/client/src/components/Collection/Collection.tsx @@ -86,7 +86,7 @@ const Table = ({ return ( {/* Placeholder cells for alignment */} - + {release.release_id} {release.xml_file_path} diff --git a/packages/ddex/webapp/server/package.json b/packages/ddex/webapp/server/package.json index 04214f104fa..3a207c23702 100644 --- a/packages/ddex/webapp/server/package.json +++ b/packages/ddex/webapp/server/package.json @@ -1,6 +1,6 @@ { "name": "@audius/ddex-webapp-server", - "version": "0.0.16", + "version": "0.0.17", "description": "Backend server for DDEX ingestion (also serves frontend as static assets)", "main": "index.js", "scripts": { diff --git a/packages/ddex/webapp/server/src/app.ts b/packages/ddex/webapp/server/src/app.ts index 47c909d81cc..57e1176660a 100644 --- a/packages/ddex/webapp/server/src/app.ts +++ b/packages/ddex/webapp/server/src/app.ts @@ -175,7 +175,7 @@ export default function createApp( env: process.env.NODE_ENV, awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID, awsBucketRaw: process.env.AWS_BUCKET_RAW, - awsBucketIndexed: process.env.AWS_BUCKET_INDEXED, + awsBucketCrawled: process.env.AWS_BUCKET_CRAWLED, ddexKey: process.env.DDEX_KEY, ddexChoreography: process.env.DDEX_CHOREOGRAPHY, }, diff --git a/packages/discovery-provider/.version.json b/packages/discovery-provider/.version.json index 904868a756e..34fb9c5995a 100644 --- a/packages/discovery-provider/.version.json +++ b/packages/discovery-provider/.version.json @@ -1,4 +1,4 @@ { - "version": "0.6.64", + "version": "0.6.67", "service": "discovery-node" } diff --git a/packages/discovery-provider/ddl/migrations/0063_ddex_jsonb_columns.sql b/packages/discovery-provider/ddl/migrations/0063_ddex_jsonb_columns.sql new file mode 100644 index 00000000000..a6e3609a1dc --- /dev/null +++ b/packages/discovery-provider/ddl/migrations/0063_ddex_jsonb_columns.sql @@ -0,0 +1,39 @@ +DO $$ +BEGIN + -- For the 'artists' column in the 'tracks' table + IF EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = 'tracks' AND column_name = 'artists' + AND udt_name != 'jsonb' + ) THEN + EXECUTE 'ALTER TABLE tracks ALTER COLUMN artists TYPE jsonb USING artists::text::jsonb;'; + END IF; + + -- For the 'resource_contributors' column in the 'tracks' table + IF EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = 'tracks' AND column_name = 'resource_contributors' + AND udt_name != 'jsonb' + ) THEN + EXECUTE 'ALTER TABLE tracks ALTER COLUMN resource_contributors TYPE jsonb USING resource_contributors::text::jsonb;'; + END IF; + + -- For the 'indirect_resource_contributors' column in the 'tracks' table + IF EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = 'tracks' AND column_name = 'indirect_resource_contributors' + AND udt_name != 'jsonb' + ) THEN + EXECUTE 'ALTER TABLE tracks ALTER COLUMN indirect_resource_contributors TYPE jsonb USING indirect_resource_contributors::text::jsonb;'; + END IF; + + -- For the 'artists' column in the 'playlists' table + IF EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = 'playlists' AND column_name = 'artists' + AND udt_name != 'jsonb' + ) THEN + EXECUTE 'ALTER TABLE playlists ALTER COLUMN artists TYPE jsonb USING artists::text::jsonb;'; + END IF; + +END $$; diff --git a/packages/discovery-provider/plugins/pedalboard/apps/app-template/package.json b/packages/discovery-provider/plugins/pedalboard/apps/app-template/package.json index ac1d4d5de2c..8add63ec2ee 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/app-template/package.json +++ b/packages/discovery-provider/plugins/pedalboard/apps/app-template/package.json @@ -1,6 +1,6 @@ { "name": "@pedalboard/app-template", - "version": "0.0.26", + "version": "0.0.27", "private": true, "scripts": { "build": "tsc", diff --git a/packages/discovery-provider/plugins/pedalboard/apps/relay/package.json b/packages/discovery-provider/plugins/pedalboard/apps/relay/package.json index 6760758fdd4..5f4942032b4 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/relay/package.json +++ b/packages/discovery-provider/plugins/pedalboard/apps/relay/package.json @@ -1,6 +1,6 @@ { "name": "@pedalboard/relay", - "version": "0.1.26", + "version": "0.1.27", "private": true, "scripts": { "build": "tsc", diff --git a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/antiAbuse.ts b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/antiAbuse.ts index d17cadd384f..84220f2aae9 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/antiAbuse.ts +++ b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/antiAbuse.ts @@ -1,13 +1,12 @@ import axios from 'axios' import { Users } from '@pedalboard/storage' import { AntiAbuseConfig } from '../config/antiAbuseConfig' -import { logger } from '../logger' import { NextFunction, Request, Response } from 'express' import { config } from '..' import { antiAbuseError, internalError } from '../error' import { readAAOState, storeAAOState } from '../redis' -import { StatusCodes } from 'http-status-codes' import { unknownToError } from '../utils' +import pino from 'pino' type AbuseRule = { rule: number @@ -28,23 +27,23 @@ export const antiAbuseMiddleware = async ( next: NextFunction ) => { const aaoConfig = config.aao - const { ip, recoveredSigner, signerIsApp, createOrDeactivate, isSenderVerifier, requestId, validatedRelayRequest } = response.locals.ctx + const { ip, recoveredSigner, signerIsApp, createOrDeactivate, isSenderVerifier, requestId, validatedRelayRequest, logger } = response.locals.ctx // no AAO to check and creates / deactivates should always be allowed if (signerIsApp || createOrDeactivate || isSenderVerifier) { - logger.info({ requestId, signerIsApp, createOrDeactivate, isSenderVerifier }, "antiabuse skipped") + logger.info({ createOrDeactivate, isSenderVerifier }, "antiabuse skipped") next() return } const user = recoveredSigner as Users // fire and async update aao cache - detectAbuse(aaoConfig, user, ip).catch((e) => { + detectAbuse(aaoConfig, logger, user, ip).catch((e) => { logger.error({ error: e }, 'AAO uncaught exception') }) if (!user.handle) { - logger.error({ requestId, user, validatedRelayRequest }, "user found without handle") + logger.error({ validatedRelayRequest }, "user found without handle") internalError( next, `user found but without handle, investigate ${JSON.stringify(user)}` @@ -61,7 +60,7 @@ export const antiAbuseMiddleware = async ( } if (userAbuseRules?.blockedFromRelay) { - logger.info({ requestId, address: user.wallet, userId: user.user_id, handle: user.handle_lc }, `blocked from relay ${user.handle_lc}`) + logger.info(`blocked from relay ${user.handle_lc}`) antiAbuseError(next, 'blocked from relay') return } @@ -73,6 +72,7 @@ export const antiAbuseMiddleware = async ( // detects abuse for the queried user and stores in cache export const detectAbuse = async ( aaoConfig: AntiAbuseConfig, + logger: pino.Logger, user: Users, reqIp: string ) => { diff --git a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/contextInjector.ts b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/contextInjector.ts index dc9ae2de5f6..92e65e27b14 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/contextInjector.ts +++ b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/contextInjector.ts @@ -1,5 +1,6 @@ import { DeveloperApps, Users } from '@pedalboard/storage' import { ValidatedRelayRequest } from '../types/relay' +import pino from 'pino' /** Context built by the context injector that can be referenced later in the request (via response object). */ export interface RequestContext { @@ -15,6 +16,7 @@ export interface RequestContext { signerIsUser: boolean createOrDeactivate: boolean isSenderVerifier: boolean + logger: pino.Logger } declare global { diff --git a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/errorHandler.ts b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/errorHandler.ts index dbfa87650c2..d3ce9990196 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/errorHandler.ts +++ b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/errorHandler.ts @@ -1,6 +1,5 @@ import { NextFunction, Request, Response } from "express"; import { AppError, isAppError } from "../error"; -import { logger } from "../logger"; import { StatusCodes } from "http-status-codes"; import { outgoingLog } from "./logging"; @@ -10,7 +9,7 @@ export const errorHandler = ( response: Response, next: NextFunction ) => { - const { requestId, validatedRelayRequest } = response.locals.ctx + const { requestId, validatedRelayRequest, logger } = response.locals.ctx // if unknown error is thrown somewhere if (!isAppError(error)) { logger.error({ requestId, error, validatedRelayRequest }, "unhandled error occured"); diff --git a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/logging.ts b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/logging.ts index 5ed3739059b..12460e7f55c 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/logging.ts +++ b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/logging.ts @@ -10,7 +10,8 @@ export const incomingRequestLogger = ( const startTime = new Date(new Date().getTime()); const requestId = typeof request.headers["X-Request-Id"] === "string" ? request.headers["X-Request-ID"] as string : uuidv4(); const oldCtx = response.locals.ctx; - response.locals.ctx = { ...oldCtx, startTime, requestId }; + const requestLogger = logger.child({ startTime, requestId }) + response.locals.ctx = { ...oldCtx, startTime, requestId, logger: requestLogger }; const { route, method } = request; const path: string = route.path @@ -30,8 +31,8 @@ export const outgoingLog = (request: Request, response: Response) => { const statusCode = response.statusCode; const path: string = route.path if (!path.includes("health")) { - logger.info( - { requestId, path, method, responseTime, statusCode }, + response.locals.ctx.logger.info( + { responseTime, statusCode }, "request completed" ); } diff --git a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/rateLimiter.ts b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/rateLimiter.ts index 17a382a749c..77771986717 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/rateLimiter.ts +++ b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/rateLimiter.ts @@ -5,7 +5,6 @@ import { DeveloperApps, Table, Users } from '@pedalboard/storage' import { config } from '..' import { NextFunction, Request, Response, response } from 'express' import { rateLimitError } from '../error' -import { logger } from '../logger' const globalRateLimiter = new RelayRateLimiter() @@ -14,7 +13,7 @@ export const rateLimiterMiddleware = async ( res: Response, next: NextFunction ) => { - const { validatedRelayRequest, recoveredSigner, signerIsUser, createOrDeactivate, isSenderVerifier, requestId } = res.locals.ctx + const { validatedRelayRequest, recoveredSigner, signerIsUser, createOrDeactivate, isSenderVerifier, logger } = res.locals.ctx const { encodedABI } = validatedRelayRequest let signer: string | null @@ -58,12 +57,12 @@ export const rateLimiterMiddleware = async ( signer, limit }) - logger.info({ requestId, address: signer, operation, limit }, "calculated rate limit") + logger.info({ limit }, "calculated rate limit") insertReplyHeaders(res, rateLimitData) } catch (e) { if (e instanceof RateLimiterRes) { insertReplyHeaders(res, e as RateLimiterRes) - logger.info({ requestId, address: signer, operation, limit }, "rate limit hit") + logger.info({ limit }, "rate limit hit") rateLimitError(next, 'rate limit hit') return } diff --git a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/validator.ts b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/validator.ts index ad2c6e96747..2c0cc58ceec 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/validator.ts +++ b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/middleware/validator.ts @@ -4,7 +4,6 @@ import { validationError } from '../error' import { DeveloperApps, Table, Users } from '@pedalboard/storage' import { AudiusABIDecoder } from '@audius/sdk' import { config, discoveryDb } from '..' -import { logger } from '../logger' import { isUserCreate, isUserDeactivate } from '../utils' import { getEntityManagerActionKey } from './rateLimiter' @@ -16,7 +15,7 @@ export const validator = async ( next: NextFunction ) => { const body = request.body as RelayRequest - const { requestId } = response.locals.ctx + const { logger } = response.locals.ctx // Validation of input fields const contractAddress = @@ -51,8 +50,19 @@ export const validator = async ( handle } + const loggerInfo: { + operation?: string, + handle?: string, + address?: string, + userId?: number, + isApp?: boolean + } = { + isApp: false + } + const operation = getEntityManagerActionKey(encodedABI) - logger.info({ requestId, operation, abi: encodedABI }) + loggerInfo.operation = operation + logger.info({ operation, encodedABI }, "retrieved operation") // Gather user from input data // @ts-ignore, partially populate for now @@ -76,19 +86,23 @@ export const validator = async ( if (user !== undefined) { recoveredSigner = user signerIsUser = true - logger.info({ requestId, handle: user.handle_lc, address: user.wallet, userId: user.user_id, operation }, `retrieved user ${user.handle_lc}`) + loggerInfo.handle = user.handle_lc || undefined + loggerInfo.address = user.wallet || undefined + loggerInfo.userId = user.user_id || undefined + + logger.info({ handle: user.handle_lc, address: user.wallet, userId: user.user_id, operation }, `retrieved user ${user.handle_lc}`) } if (signerIsUser) { const isDeactivated = (recoveredSigner as Users).is_deactivated if (isUserDeactivate(isDeactivated, encodedABI)) { - logger.info({ requestId, encodedABI, operation }, "user deactivation") + logger.info("user deactivation") createOrDeactivate = true } } if (isUserCreate(encodedABI)) { - logger.info({ requestId: response.locals.ctx.requestId, encodedABI, operation }, "user create") + logger.info("user create") createOrDeactivate = true } @@ -96,19 +110,25 @@ export const validator = async ( if (!signerIsUser && !createOrDeactivate && !isSenderVerifier) { const developerApp = await retrieveDeveloperApp({ encodedABI, contractAddress }) if (developerApp === undefined) { - logger.error({ encodedABI, operation }, "neither user nor developer app could be found for address") + logger.error("neither user nor developer app could be found for address") validationError(next, 'recoveredSigner not valid') return } recoveredSigner = developerApp signerIsApp = true - logger.info({ requestId, address: developerApp.address, userId: developerApp.user_id, name: developerApp.name, operation }, `retrieved developer app ${developerApp.name}`) + loggerInfo.address = developerApp.address + loggerInfo.userId = developerApp.user_id || undefined + loggerInfo.handle = developerApp.name + loggerInfo.isApp = true + logger.info({ address: developerApp.address, userId: developerApp.user_id, handle: developerApp.name, operation }, `retrieved developer app ${developerApp.name}`) } // inject remaining fields into ctx for downstream middleware const ip = request.ip ?? '' const oldCtx = response.locals.ctx + // create child logger with additional + const newLogger = logger.child({...loggerInfo}) response.locals.ctx = { ...oldCtx, validatedRelayRequest, @@ -117,7 +137,8 @@ export const validator = async ( ip, signerIsApp, signerIsUser, - isSenderVerifier + isSenderVerifier, + logger: newLogger } next() } diff --git a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/txRelay.ts b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/txRelay.ts index c05d230dd8f..34bda7f8cc0 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/txRelay.ts +++ b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/txRelay.ts @@ -1,13 +1,11 @@ import { config, wallets, web3 } from "."; import { internalError } from "./error"; -import { logger } from "./logger"; import { confirm } from "./web3"; import { TransactionReceipt, TransactionRequest, } from "@ethersproject/abstract-provider"; import { NextFunction, Request, Response } from "express"; -import { unknownToError } from "./utils"; export type RelayedTransaction = { receipt: TransactionReceipt; @@ -20,7 +18,7 @@ export const relayTransaction = async ( next: NextFunction ) => { // pull info from validated request - const { validatedRelayRequest, requestId } = res.locals.ctx; + const { validatedRelayRequest, logger } = res.locals.ctx; const { encodedABI, gasLimit, contractAddress } = validatedRelayRequest; const senderWallet = wallets.selectNextWallet(); @@ -39,17 +37,17 @@ export const relayTransaction = async ( const transaction = { nonce, gasLimit, to, value, data }; await senderWallet.signTransaction(transaction); - logger.info({ requestId, senderWallet: address, nonce }, "submitting transaction") + logger.info({ senderWallet: address, nonce }, "submitting transaction") submit = await senderWallet.sendTransaction(transaction); // query chain until tx is mined const receipt = await confirm(submit.hash); receipt.blockNumber += config.finalPoaBlock - logger.info({ requestId, senderWallet: address, nonce, txHash: submit?.hash, blocknumber: receipt.blockNumber }, "transaction confirmation successful") + logger.info({ senderWallet: address, nonce, txHash: submit?.hash, blocknumber: receipt.blockNumber }, "transaction confirmation successful") res.send({ receipt }); } catch (e) { - logger.error({ requestId, senderWallet: address, nonce, txHash: submit?.hash }, "transaction confirmation failed") + logger.error({ senderWallet: address, nonce, txHash: submit?.hash }, "transaction confirmation failed") internalError(next, e); return; } diff --git a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/web3.ts b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/web3.ts index 59214f64663..43360183484 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/relay/src/web3.ts +++ b/packages/discovery-provider/plugins/pedalboard/apps/relay/src/web3.ts @@ -35,13 +35,13 @@ export const connectWeb3 = async (config: Config): Promise => { return { web3, chainId: chainId.toString() }; }; -export const confirm = async (txHash: string | undefined, retries = 24): Promise => { +export const confirm = async (txHash: string | undefined, retries = 128): Promise => { if (txHash === undefined) throw new Error("transaction hash not defined") let tries = 0; while (tries !== retries) { const receipt = await web3js.eth.getTransactionReceipt(txHash); if (receipt !== null && receipt.status) return receipt; - await delay(250); + await delay(500); tries += 1; } throw new Error(`transaction ${txHash} could not be confirmed`); diff --git a/packages/discovery-provider/plugins/pedalboard/apps/sla-auditor/package.json b/packages/discovery-provider/plugins/pedalboard/apps/sla-auditor/package.json index 6adc430943d..5ebebfa0eff 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/sla-auditor/package.json +++ b/packages/discovery-provider/plugins/pedalboard/apps/sla-auditor/package.json @@ -1,6 +1,6 @@ { "name": "@pedalboard/sla-auditor", - "version": "0.0.26", + "version": "0.0.27", "private": true, "scripts": { "build": "tsc", diff --git a/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/package.json b/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/package.json index f770898e378..06d7724ad1c 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/package.json +++ b/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/package.json @@ -1,6 +1,6 @@ { "name": "@pedalboard/solana-relay", - "version": "0.0.19", + "version": "0.0.20", "private": true, "scripts": { "start": "ts-node ./src/index.ts", diff --git a/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/redis.ts b/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/redis.ts index 63c6c673e4b..6701f682762 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/redis.ts +++ b/packages/discovery-provider/plugins/pedalboard/apps/solana-relay/src/redis.ts @@ -33,7 +33,7 @@ export const cacheTransaction = async ( ) => { const redis = await getRedisConnection() const key = `solana:transaction:${signature}` - await redis.set(key, transaction) + await redis.set(key, transaction, { EX: 60 }) } export const getCachedDiscoveryNodeEndpoints = async () => { diff --git a/packages/discovery-provider/plugins/pedalboard/apps/trending-challenge-rewards/package.json b/packages/discovery-provider/plugins/pedalboard/apps/trending-challenge-rewards/package.json index 907deb733af..e53ea6443a4 100644 --- a/packages/discovery-provider/plugins/pedalboard/apps/trending-challenge-rewards/package.json +++ b/packages/discovery-provider/plugins/pedalboard/apps/trending-challenge-rewards/package.json @@ -1,6 +1,6 @@ { "name": "@pedalboard/trending-challenge-rewards", - "version": "0.0.26", + "version": "0.0.27", "private": true, "scripts": { "build": "tsc", diff --git a/packages/discovery-provider/plugins/pedalboard/packages/basekit/package.json b/packages/discovery-provider/plugins/pedalboard/packages/basekit/package.json index a608cbf1eb9..4b7db8a1a59 100644 --- a/packages/discovery-provider/plugins/pedalboard/packages/basekit/package.json +++ b/packages/discovery-provider/plugins/pedalboard/packages/basekit/package.json @@ -1,6 +1,6 @@ { "name": "@pedalboard/basekit", - "version": "0.0.26", + "version": "0.0.27", "private": true, "main": "./dist/index.js", "source": "./src/index.ts", diff --git a/packages/discovery-provider/plugins/pedalboard/packages/eslint-config-custom-server/package.json b/packages/discovery-provider/plugins/pedalboard/packages/eslint-config-custom-server/package.json index c3634133283..2ece00b788d 100644 --- a/packages/discovery-provider/plugins/pedalboard/packages/eslint-config-custom-server/package.json +++ b/packages/discovery-provider/plugins/pedalboard/packages/eslint-config-custom-server/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-custom-server", - "version": "0.0.26", + "version": "0.0.27", "license": "MIT", "main": "index.js", "publishConfig": { diff --git a/packages/discovery-provider/plugins/pedalboard/packages/eslint-config-custom/package.json b/packages/discovery-provider/plugins/pedalboard/packages/eslint-config-custom/package.json index 1d4c5297fa0..88a095876d9 100644 --- a/packages/discovery-provider/plugins/pedalboard/packages/eslint-config-custom/package.json +++ b/packages/discovery-provider/plugins/pedalboard/packages/eslint-config-custom/package.json @@ -1,7 +1,7 @@ { "name": "eslint-config-custom", "private": true, - "version": "0.0.26", + "version": "0.0.27", "license": "MIT", "main": "index.js", "dependencies": { diff --git a/packages/discovery-provider/plugins/pedalboard/packages/jest-presets/package.json b/packages/discovery-provider/plugins/pedalboard/packages/jest-presets/package.json index 6552e01ad15..2b44101f0a1 100644 --- a/packages/discovery-provider/plugins/pedalboard/packages/jest-presets/package.json +++ b/packages/discovery-provider/plugins/pedalboard/packages/jest-presets/package.json @@ -1,6 +1,6 @@ { "name": "jest-presets", - "version": "0.0.26", + "version": "0.0.27", "private": true, "license": "MIT", "dependencies": { diff --git a/packages/discovery-provider/plugins/pedalboard/packages/logger/package.json b/packages/discovery-provider/plugins/pedalboard/packages/logger/package.json index 77935609b25..6309849d6a0 100644 --- a/packages/discovery-provider/plugins/pedalboard/packages/logger/package.json +++ b/packages/discovery-provider/plugins/pedalboard/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@pedalboard/logger", - "version": "0.0.26", + "version": "0.0.27", "private": true, "main": "./dist/index.js", "source": "./src/index.ts", diff --git a/packages/discovery-provider/plugins/pedalboard/packages/storage/package.json b/packages/discovery-provider/plugins/pedalboard/packages/storage/package.json index d1a6de06f43..a15337e1be3 100644 --- a/packages/discovery-provider/plugins/pedalboard/packages/storage/package.json +++ b/packages/discovery-provider/plugins/pedalboard/packages/storage/package.json @@ -1,6 +1,6 @@ { "name": "@pedalboard/storage", - "version": "0.0.26", + "version": "0.0.27", "private": true, "main": "./dist/index.js", "source": "./src/index.ts", diff --git a/packages/discovery-provider/plugins/pedalboard/packages/tsconfig/package.json b/packages/discovery-provider/plugins/pedalboard/packages/tsconfig/package.json index a9deca4ecf4..5909fdaba17 100644 --- a/packages/discovery-provider/plugins/pedalboard/packages/tsconfig/package.json +++ b/packages/discovery-provider/plugins/pedalboard/packages/tsconfig/package.json @@ -1,6 +1,6 @@ { "name": "tsconfig", - "version": "0.0.26", + "version": "0.0.27", "private": true, "license": "MIT", "publishConfig": { diff --git a/packages/discovery-provider/src/api/v1/helpers.py b/packages/discovery-provider/src/api/v1/helpers.py index 8ec2077c0c3..41535342571 100644 --- a/packages/discovery-provider/src/api/v1/helpers.py +++ b/packages/discovery-provider/src/api/v1/helpers.py @@ -383,6 +383,14 @@ def extend_track(track): "is_deactivated" ) + # todo: remove once clients catch up i.e. no longer use this field + track["download"] = { + "cid": track.get("track_cid"), + "is_downloadable": bool(track.get("is_downloadable")), + "requires_follow": track.get("download_conditions") is not None + and "follow_user_id" in track.get("download_conditions"), + } + # TODO: This block is only for legacy tracks that have track_segments instead of duration duration = track.get("duration") if not duration: diff --git a/packages/discovery-provider/src/models/playlists/playlist.py b/packages/discovery-provider/src/models/playlists/playlist.py index e7e381e8607..dd88397c181 100644 --- a/packages/discovery-provider/src/models/playlists/playlist.py +++ b/packages/discovery-provider/src/models/playlists/playlist.py @@ -1,5 +1,4 @@ from sqlalchemy import ( - ARRAY, Boolean, Column, DateTime, @@ -58,7 +57,7 @@ class Playlist(Base, RepresentableMixin): # From DDEX ddex_app = Column(String) ddex_release_ids = Column(JSONB()) - artists = Column(ARRAY(JSONB())) + artists = Column(JSONB()) copyright_line = Column(JSONB()) producer_copyright_line = Column(JSONB()) parental_warning_type = Column(String) diff --git a/packages/discovery-provider/src/models/tracks/track.py b/packages/discovery-provider/src/models/tracks/track.py index f17db02766e..2d1d544f034 100644 --- a/packages/discovery-provider/src/models/tracks/track.py +++ b/packages/discovery-provider/src/models/tracks/track.py @@ -95,9 +95,9 @@ class Track(Base, RepresentableMixin): # From DDEX ddex_release_ids = Column(JSONB()) ddex_app = Column(String) - artists = Column(ARRAY(JSONB())) - resource_contributors = Column(ARRAY(JSONB())) - indirect_resource_contributors = Column(ARRAY(JSONB())) + artists = Column(JSONB()) + resource_contributors = Column(JSONB()) + indirect_resource_contributors = Column(JSONB()) rights_controller = Column(JSONB()) copyright_line = Column(JSONB()) producer_copyright_line = Column(JSONB()) diff --git a/packages/discovery-provider/src/queries/search_es.py b/packages/discovery-provider/src/queries/search_es.py index 865ba15dbe8..218f867e9e5 100644 --- a/packages/discovery-provider/src/queries/search_es.py +++ b/packages/discovery-provider/src/queries/search_es.py @@ -23,7 +23,7 @@ def search_es_full(args: dict): if not esclient: raise Exception("esclient is None") - search_str = args.get("query") + search_str = args.get("query", "").strip() current_user_id = args.get("current_user_id") limit = args.get("limit", 10) offset = args.get("offset", 0) @@ -58,21 +58,6 @@ def search_es_full(args: dict): ] ) - # saved tracks - if current_user_id: - mdsl.extend( - [ - {"index": ES_TRACKS}, - track_dsl( - search_str=search_str, - current_user_id=current_user_id, - must_saved=True, - only_downloadable=only_downloadable, - include_purchaseable=include_purchaseable, - ), - ] - ) - # users if do_users: mdsl.extend( @@ -81,13 +66,6 @@ def search_es_full(args: dict): user_dsl(search_str, current_user_id), ] ) - if current_user_id: - mdsl.extend( - [ - {"index": ES_USERS}, - user_dsl(search_str, current_user_id, True), - ] - ) # playlists if do_playlists: @@ -98,15 +76,6 @@ def search_es_full(args: dict): ] ) - # saved playlists - if current_user_id: - mdsl.extend( - [ - {"index": ES_PLAYLISTS}, - playlist_dsl(search_str, current_user_id, True), - ] - ) - # albums if do_albums: mdsl.extend( @@ -115,14 +84,6 @@ def search_es_full(args: dict): album_dsl(search_str, current_user_id), ] ) - # saved albums - if current_user_id: - mdsl.extend( - [ - {"index": ES_PLAYLISTS}, - album_dsl(search_str, current_user_id, True), - ] - ) mdsl_limit_offset(mdsl, limit, offset) mfound = esclient.msearch(searches=mdsl) @@ -140,24 +101,15 @@ def search_es_full(args: dict): if do_tracks: response["tracks"] = pluck_hits(mfound["responses"].pop(0)) - if current_user_id: - response["saved_tracks"] = pluck_hits(mfound["responses"].pop(0)) if do_users: response["users"] = pluck_hits(mfound["responses"].pop(0)) - if current_user_id: - response["followed_users"] = pluck_hits(mfound["responses"].pop(0)) if do_playlists: response["playlists"] = pluck_hits(mfound["responses"].pop(0)) - if current_user_id: - response["saved_playlists"] = pluck_hits(mfound["responses"].pop(0)) if do_albums: response["albums"] = pluck_hits(mfound["responses"].pop(0)) - if current_user_id: - response["saved_albums"] = pluck_hits(mfound["responses"].pop(0)) - finalize_response( response, limit, current_user_id, is_auto_complete=is_auto_complete ) @@ -226,7 +178,7 @@ def tag_match(fieldname, sort_by): def mdsl_limit_offset(mdsl, limit, offset): # add size and limit with some over-fetching - # for sake of drop_copycats + # for sake of reorder_users index_name = "" for dsl in mdsl: if "index" in dsl: @@ -296,7 +248,7 @@ def finalize_response( # users: finalize for k in ["users", "followed_users"]: - users = drop_copycats(response[k]) + users = reorder_users(response[k]) users = users[:limit] response[k] = [map_user(user, current_user) for user in users] @@ -313,7 +265,7 @@ def finalize_response( return response -def base_match(search_str: str, operator="or", extra_fields=[]): +def base_match(search_str: str, operator="or", extra_fields=[], boost=1): return [ { "multi_match": { @@ -327,17 +279,18 @@ def base_match(search_str: str, operator="or", extra_fields=[]): "operator": operator, "type": "bool_prefix", "fuzziness": "AUTO", + "boost": boost, } } ] def be_saved(current_user_id): - return {"term": {"saved_by": {"value": current_user_id, "boost": 1.2}}} + return {"term": {"saved_by": {"value": current_user_id, "boost": 4}}} def be_reposted(current_user_id): - return {"term": {"reposted_by": {"value": current_user_id, "boost": 1.2}}} + return {"term": {"reposted_by": {"value": current_user_id, "boost": 4}}} def be_followed(current_user_id): @@ -348,6 +301,7 @@ def be_followed(current_user_id): "id": str(current_user_id), "path": "following_ids", }, + "boost": 500, } } @@ -361,14 +315,14 @@ def personalize_dsl(dsl, current_user_id, must_saved): dsl["should"].append(be_reposted(current_user_id)) -def default_function_score(dsl, ranking_field): +def default_function_score(dsl, ranking_field, factor=0.1): return { "query": { "function_score": { "query": {"bool": dsl}, "field_value_factor": { "field": ranking_field, - "factor": 20, + "factor": factor, "modifier": "ln2p", }, "boost_mode": "multiply", @@ -386,23 +340,69 @@ def track_dsl( ): dsl = { "must": [ - *base_match(search_str), {"term": {"is_unlisted": {"value": False}}}, {"term": {"is_delete": False}}, + { + "bool": { + "should": [ + *base_match(search_str), + { + "wildcard": { + "title": { + "value": "*" + search_str + "*", + "boost": 0.01, + "case_insensitive": True, + } + } + }, + { + "multi_match": { + "query": search_str, + "fields": ["title.searchable", "user.name.searchable"], + "type": "cross_fields", + "operator": "and", + "boost": len(search_str) * 0.5, + } + }, + *[ + { + "match": { + "genre": { + "query": search_str.title(), + "boost": 20, + } + } + } + ], + { + "match": { + "tag_list": { + "query": search_str.replace(" ", ""), + "boost": 0.1, + } + } + }, + *[ + { + "match": { + "mood": { + "query": search_str.title(), + "boost": 0.5, + } + } + } + ], + ], + "minimum_should_match": 1, + } + }, ], "must_not": [ {"exists": {"field": "stem_of"}}, ], "should": [ - *base_match(search_str, operator="and"), - { - "match": { - "title.searchable": { - "query": search_str, - "minimum_should_match": "100%", - }, - } - }, + *base_match(search_str, operator="and", boost=len(search_str)), + {"term": {"user.is_verified": {"value": True}}}, ], } @@ -420,15 +420,101 @@ def user_dsl(search_str, current_user_id, must_saved=False): # must_search_str = search_str + " " + search_str.replace(" ", "") dsl = { "must": [ - *base_match(search_str, extra_fields=["handle"]), {"term": {"is_deactivated": {"value": False}}}, + { + "bool": { + "should": [ + *base_match( + search_str, + extra_fields=["handle.searchable", "name.searchable"], + boost=len(search_str) * 0.1, + ), + { + "wildcard": { + "name": { + "value": "*" + search_str + "*", + "boost": 0.01, + "case_insensitive": True, + } + } + }, + { + "match": { + "name.searchable": { + "query": search_str, + "fuzziness": "AUTO", + "boost": len(search_str) * 0.01, + } + } + }, + ( + { + "term": { + "name": { + "value": search_str.replace(" ", ""), + "boost": len(search_str) * 0.5, + } + } + } + ), + { + "term": ( + { + "handle": { + "value": search_str.replace(" ", ""), + "boost": len(search_str) * 0.5, + } + } + ) + }, + { + "match": { + "tracks.genre": { + "query": search_str.title(), + "boost": 12, + } + } + }, + { + "match": { + "tracks.tags": { + "query": search_str.replace(" ", ""), + "boost": 0.1, + } + } + }, + { + "match": { + "tracks.mood": { + "query": search_str.title(), + "boost": 12, + } + } + }, + ], + "minimum_should_match": 1, + } + }, ], "must_not": [], "should": [ - *base_match(search_str, operator="and", extra_fields=["handle"]), - {"term": {"handle": {"value": search_str, "boost": 0.2}}}, - {"term": {"name": {"value": search_str, "boost": 0.2}}}, - {"term": {"is_verified": {"value": True, "boost": 1.5}}}, + *base_match( + search_str, + operator="and", + extra_fields=["name"], + boost=len(search_str) * 12, + ), + ( + { + "term": { + "name": { + "value": search_str, + "boost": (len(search_str) * 0.1) ** 2, + } + } + } + ), + {"term": {"is_verified": {"value": True, "boost": 5}}}, ], } @@ -438,20 +524,96 @@ def user_dsl(search_str, current_user_id, must_saved=False): if current_user_id: dsl["should"].append(be_followed(current_user_id)) - return default_function_score(dsl, "follower_count") + return { + "query": { + "function_score": { + "query": {"bool": dsl}, + "functions": [ + { + "filter": {"term": {"is_verified": True}}, + "field_value_factor": { + "field": "follower_count", + "factor": 10, + "modifier": "ln2p", + }, + }, + { + "filter": {"term": {"is_verified": False}}, + "field_value_factor": { + "field": "follower_count", + "factor": 0.1, + "modifier": "ln2p", + }, + }, + ], + "boost_mode": "multiply", + } + } + } def base_playlist_dsl(search_str, is_album): return { "must": [ - *base_match(search_str), + { + "bool": { + "should": [ + *base_match(search_str, boost=len(search_str)), + { + "wildcard": { + "playlist_name": { + "value": "*" + search_str + "*", + "boost": 0.01, + "case_insensitive": True, + } + } + }, + { + "multi_match": { + "query": search_str, + "fields": [ + "playlist_name.searchable", + "user.name.searchable", + ], + "type": "cross_fields", + "operator": "or", + "boost": len(search_str) * 0.5, + } + }, + { + "match": { + "tracks.tags": { + "query": search_str.replace(" ", ""), + "boost": 0.01, + } + } + }, + { + "match": { + "tracks.genre": { + "query": search_str.title(), + "boost": 20, + } + } + }, + { + "match": { + "tracks.mood": { + "query": search_str.title(), + } + } + }, + ], + "minimum_should_match": 1, + } + }, {"term": {"is_private": {"value": False}}}, {"term": {"is_delete": False}}, {"term": {"is_album": {"value": is_album}}}, ], "should": [ - *base_match(search_str, operator="and"), - {"term": {"is_verified": {"value": True}}}, + *base_match(search_str, operator="and", boost=len(search_str) * 10), + {"term": {"user.is_verified": {"value": True, "boost": 3}}}, ], } @@ -459,19 +621,21 @@ def base_playlist_dsl(search_str, is_album): def playlist_dsl(search_str, current_user_id, must_saved=False): dsl = base_playlist_dsl(search_str, False) personalize_dsl(dsl, current_user_id, must_saved) - return default_function_score(dsl, "repost_count") + return default_function_score(dsl, "repost_count", factor=1000) def album_dsl(search_str, current_user_id, must_saved=False): dsl = base_playlist_dsl(search_str, True) personalize_dsl(dsl, current_user_id, must_saved) - return default_function_score(dsl, "repost_count") + return default_function_score(dsl, "repost_count", factor=1000) -def drop_copycats(users): +def reorder_users(users): """Filters out users with copy cat names. e.g. if a verified deadmau5 is in the result set filter out all non-verified users with same name. + + Moves users without profile pictures to the end. """ reserved = set() for user in users: @@ -479,11 +643,16 @@ def drop_copycats(users): reserved.add(lower_ascii_name(user["name"])) filtered = [] + users_without_photos = [] for user in users: if not user["is_verified"] and lower_ascii_name(user["name"]) in reserved: continue - filtered.append(user) - return filtered + if user["profile_picture_sizes"] or user["profile_picture"]: + filtered.append(user) + else: + users_without_photos.append(user) + + return filtered + users_without_photos def lower_ascii_name(name): diff --git a/packages/discovery-provider/src/tasks/entity_manager/entities/playlist.py b/packages/discovery-provider/src/tasks/entity_manager/entities/playlist.py index d8a452b10f2..ec598a7525c 100644 --- a/packages/discovery-provider/src/tasks/entity_manager/entities/playlist.py +++ b/packages/discovery-provider/src/tasks/entity_manager/entities/playlist.py @@ -466,6 +466,10 @@ def create_playlist(params: ManageEntityParameters): is_current=False, is_delete=False, ddex_app=ddex_app, + ddex_release_ids=params.metadata.get("ddex_release_ids", None), + artists=params.metadata.get("artists", None), + copyright_line=params.metadata.get("copyright_line", None), + producer_copyright_line=params.metadata.get("producer_copyright_line", None), parental_warning_type=params.metadata.get("parental_warning_type", None), ) diff --git a/packages/discovery-provider/src/tasks/entity_manager/entities/track.py b/packages/discovery-provider/src/tasks/entity_manager/entities/track.py index 619ac7802e5..24c9c21afa1 100644 --- a/packages/discovery-provider/src/tasks/entity_manager/entities/track.py +++ b/packages/discovery-provider/src/tasks/entity_manager/entities/track.py @@ -329,32 +329,74 @@ def populate_track_record_metadata(track_record: Track, track_metadata, handle, ) elif key == "ddex_release_ids": - if "ddex_release_ids" in track_metadata and track_metadata["ddex_release_ids"]: - continue + if "ddex_release_ids" in track_metadata and ( + is_valid_json_field(track_metadata, "ddex_release_ids") + or track_metadata["ddex_release_ids"] is None + ): + track_record.ddex_release_ids = track_metadata["ddex_release_ids"] elif key == "artists": - if "artists" in track_metadata and track_metadata["artists"]: - continue + if "artists" in track_metadata: + artists = track_metadata["artists"] + if artists and isinstance(artists, list): + valid = True + for artist in artists: + if not isinstance(artist, dict): + valid = False + break + if valid: + track_record.artists = artists + elif artists is None: + track_record.artists = artists elif key == "resource_contributors": - if "resource_contributors" in track_metadata and track_metadata["resource_contributors"]: - continue + if "resource_contributors" in track_metadata: + resource_contributors = track_metadata["resource_contributors"] + if resource_contributors and isinstance(resource_contributors, list): + valid = True + for contributor in resource_contributors: + if not isinstance(contributor, dict): + valid = False + break + if valid: + track_record.resource_contributors = resource_contributors + elif resource_contributors is None: + track_record.resource_contributors = resource_contributors elif key == "indirect_resource_contributors": - if "indirect_resource_contributors" in track_metadata and track_metadata["indirect_resource_contributors"]: - continue + if "indirect_resource_contributors" in track_metadata: + indirect_resource_contributors = track_metadata["indirect_resource_contributors"] + if indirect_resource_contributors and isinstance(indirect_resource_contributors, list): + valid = True + for contributor in indirect_resource_contributors: + if not isinstance(contributor, dict): + valid = False + break + if valid: + track_record.indirect_resource_contributors = indirect_resource_contributors + elif indirect_resource_contributors is None: + track_record.indirect_resource_contributors = indirect_resource_contributors elif key == "rights_controller": - if "rights_controller" in track_metadata and track_metadata["rights_controller"]: - continue + if "rights_controller" in track_metadata and ( + is_valid_json_field(track_metadata, "rights_controller") + or track_metadata["rights_controller"] is None + ): + track_record.rights_controller = track_metadata["rights_controller"] elif key == "copyright_line": - if "copyright_line" in track_metadata and track_metadata["copyright_line"]: - continue + if "copyright_line" in track_metadata and ( + is_valid_json_field(track_metadata, "copyright_line") + or track_metadata["copyright_line"] is None + ): + track_record.copyright_line = track_metadata["copyright_line"] elif key == "producer_copyright_line": - if "producer_copyright_line" in track_metadata and track_metadata["producer_copyright_line"]: - continue + if "producer_copyright_line" in track_metadata and ( + is_valid_json_field(track_metadata, "producer_copyright_line") + or track_metadata["producer_copyright_line"] is None + ): + track_record.producer_copyright_line = track_metadata["producer_copyright_line"] else: # For most fields, update the track_record when the corresponding field exists diff --git a/packages/dotenv-linter/package.json b/packages/dotenv-linter/package.json index 49e7e48acb4..5c4a6ca7324 100644 --- a/packages/dotenv-linter/package.json +++ b/packages/dotenv-linter/package.json @@ -1,7 +1,7 @@ { "name": "@audius/dotenv-linter", "private": true, - "version": "1.5.71", + "version": "1.5.72", "description": "dotenv lint", "author": "Audius", "homepage": "https://github.com/AudiusProject/audius-protocol#readme", diff --git a/packages/embed/package.json b/packages/embed/package.json index 07b9119c1f9..63a7cad0c43 100644 --- a/packages/embed/package.json +++ b/packages/embed/package.json @@ -1,6 +1,6 @@ { "name": "embed", - "version": "1.5.72", + "version": "1.5.73", "private": true, "scripts": { "start": "vite", diff --git a/packages/es-indexer/package.json b/packages/es-indexer/package.json index 2d3e6b251da..460ce008ce1 100644 --- a/packages/es-indexer/package.json +++ b/packages/es-indexer/package.json @@ -1,6 +1,6 @@ { "name": "es-indexer", - "version": "1.0.19", + "version": "1.0.20", "private": true, "description": "", "scripts": { diff --git a/packages/eslint-config-audius/package.json b/packages/eslint-config-audius/package.json index c7be6722b9a..f7f5101fe87 100644 --- a/packages/eslint-config-audius/package.json +++ b/packages/eslint-config-audius/package.json @@ -1,7 +1,7 @@ { "name": "eslint-config-audius", "private": true, - "version": "1.5.72", + "version": "1.5.73", "description": "Custom eslint config for Audius.", "author": "Audius", "homepage": "https://github.com/AudiusProject/audius-client#readme", diff --git a/packages/fixed-decimal/package.json b/packages/fixed-decimal/package.json index 897bf00312f..a5e69fb05ee 100644 --- a/packages/fixed-decimal/package.json +++ b/packages/fixed-decimal/package.json @@ -1,6 +1,6 @@ { "name": "@audius/fixed-decimal", - "version": "0.0.21", + "version": "0.0.22", "description": "A data structure to represent fixed precision decimals", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/packages/harmony/package.json b/packages/harmony/package.json index 6b1f185053e..b2b081d999c 100644 --- a/packages/harmony/package.json +++ b/packages/harmony/package.json @@ -1,6 +1,6 @@ { "name": "@audius/harmony", - "version": "0.0.30", + "version": "0.0.31", "description": "The Audius Design System", "license": "ISC", "author": "Audius", diff --git a/packages/harmony/src/components/button/Button/Button.tsx b/packages/harmony/src/components/button/Button/Button.tsx index 9f357b788ef..5411c965dd0 100644 --- a/packages/harmony/src/components/button/Button/Button.tsx +++ b/packages/harmony/src/components/button/Button/Button.tsx @@ -108,12 +108,7 @@ export const Button = forwardRef( '&:hover': secondaryHoverStyles, '&:active': secondaryHoverStyles, - ...((_isHovered || _isPressed) && secondaryHoverStyles), - ...(isDisabled && { - backgroundColor: themeColors.neutral.n150, - color: themeColors.static.white, - boxShadow: 'none' - }) + ...((_isHovered || _isPressed) && secondaryHoverStyles) } const tertiaryHoverStyles: CSSObject = { @@ -138,34 +133,7 @@ export const Button = forwardRef( '&:active': tertiaryActiveStyles, ...(_isHovered && tertiaryHoverStyles), - ...(_isPressed && tertiaryActiveStyles), - ...(isDisabled && { - opacity: 0.45 - }) - } - - const commonHoverStyles: CSSObject = { - backgroundColor: themeColors.neutral.n25 - } - - const commonActiveStyles: CSSObject = { - backgroundColor: themeColors.neutral.n100 - } - - const commonStyles: CSSObject = { - backgroundColor: themeColors.special.white, - color: themeColors.text.default, - boxShadow: `0 0 0 1px inset ${themeColors.border.strong}`, - - '&:hover': commonHoverStyles, - '&:active': commonActiveStyles, - - ...(_isHovered && commonHoverStyles), - ...(_isPressed && commonActiveStyles), - ...(isDisabled && { - backgroundColor: themeColors.neutral.n150, - boxShadow: 'none' - }) + ...(_isPressed && tertiaryActiveStyles) } const destructiveHoverStyles: CSSObject = { @@ -180,10 +148,7 @@ export const Button = forwardRef( '&:hover': destructiveHoverStyles, '&:active': destructiveHoverStyles, - ...((_isHovered || _isPressed) && destructiveHoverStyles), - ...(isDisabled && { - opacity: 0.45 - }) + ...((_isHovered || _isPressed) && destructiveHoverStyles) } const hoverStyles: CSSObject = { @@ -219,8 +184,6 @@ export const Button = forwardRef( ? tertiaryStyles : variant === 'destructive' ? destructiveStyles - : variant === 'common' - ? commonStyles : primaryStyles), '::before': { @@ -234,13 +197,16 @@ export const Button = forwardRef( backgroundColor: 'rgba(0, 0, 0, 0)' }, - ...(variant !== 'tertiary' && - variant !== 'common' && { - ':hover': hoverStyles, - ':active': activeStyles, - ...(_isHovered && hoverStyles), - ...(_isPressed && activeStyles) - }) + ...(variant !== 'tertiary' && { + ':hover': hoverStyles, + ':active': activeStyles, + ...(_isHovered && hoverStyles), + ...(_isPressed && activeStyles) + }), + + ...(isDisabled && { + opacity: 0.45 + }) } const iconCss = diff --git a/packages/harmony/src/components/button/Button/types.ts b/packages/harmony/src/components/button/Button/types.ts index 5cf5d095e0a..ee7927e218c 100644 --- a/packages/harmony/src/components/button/Button/types.ts +++ b/packages/harmony/src/components/button/Button/types.ts @@ -6,12 +6,7 @@ import { BaseButtonProps } from '../BaseButton/types' export type HTMLButtonProps = ComponentPropsWithoutRef<'button'> -export type ButtonVariant = - | 'primary' - | 'secondary' - | 'tertiary' - | 'destructive' - | 'common' +export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'destructive' export type ButtonSize = 'small' | 'default' | 'large' diff --git a/packages/harmony/src/components/button/FollowButton/FollowButton.tsx b/packages/harmony/src/components/button/FollowButton/FollowButton.tsx index 07afeac0e38..07b6c5483aa 100644 --- a/packages/harmony/src/components/button/FollowButton/FollowButton.tsx +++ b/packages/harmony/src/components/button/FollowButton/FollowButton.tsx @@ -106,27 +106,60 @@ export const FollowButton = forwardRef( text = messages.unfollow } - const { color, cornerRadius } = useTheme() + const { color, cornerRadius, motion, shadows } = useTheme() const textColor = checkedValue || isHovering || isPressing ? color.static.white : color.primary.primary + const borderRadius = + variant === 'pill' ? cornerRadius['2xl'] : cornerRadius.s + const rootCss: CSSObject = { cursor: 'pointer', minWidth: size === 'small' ? 128 : 152, width: fullWidth ? '100%' : undefined, userSelect: 'none', - borderRadius: variant === 'pill' ? cornerRadius['2xl'] : cornerRadius.s, - background: isPressing - ? color.primary.p500 - : checkedValue || isHovering + borderRadius, + backgroundColor: checkedValue ? color.primary.primary - : color.static.white, - border: `1px solid ${ - isPressing ? color.primary.p500 : color.primary.primary - }` + : color.special.white, + boxShadow: shadows.near, + border: `1px solid ${color.primary.primary}`, + transition: ` + transform ${motion.hover}, + border-color ${motion.hover}, + background-color ${motion.hover}, + color ${motion.hover} + `, + '::before': { + zIndex: -1, + content: '""', + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0)', + borderRadius + }, + '&:hover': { + backgroundColor: color.primary.primary, + borderWidth: 0, + boxShadow: shadows.mid, + '&::before': { + backgroundColor: 'rgba(255, 255, 255, 0.2)' + } + }, + '&:active': { + backgroundColor: color.primary.primary, + borderWidth: 0, + boxShadow: 'none', + '&::before': { + backgroundColor: 'rgba(0, 0, 0, 0.2)' + } + } } // Handles case where user mouses down, moves cursor, and mouses up @@ -173,6 +206,7 @@ export const FollowButton = forwardRef( & - Pick + Pick< + BaseButtonProps, + 'onClick' | 'disabled' | 'className' | 'type' | 'children' + > /** * The icon component allows you to pass in an icon and @@ -30,6 +33,7 @@ export const IconButton = forwardRef( height, width, iconCss, + children, ...other } = props const { disabled } = other @@ -72,6 +76,7 @@ export const IconButton = forwardRef( width={width} css={iconCss} /> + {children} ) } diff --git a/packages/harmony/src/components/layout/Popup/Popup.tsx b/packages/harmony/src/components/layout/Popup/Popup.tsx index da8d70e3039..c409c2df005 100644 --- a/packages/harmony/src/components/layout/Popup/Popup.tsx +++ b/packages/harmony/src/components/layout/Popup/Popup.tsx @@ -15,6 +15,7 @@ import { useTransition, animated } from 'react-spring' import { PlainButton } from 'components/button' import { IconClose } from 'icons' +import { ModalState } from 'utils/modalState' import { useClickOutside } from '../../../hooks/useClickOutside' import { getScrollParent } from '../../../utils/getScrollParent' @@ -194,7 +195,7 @@ export const Popup = forwardRef(function Popup( checkIfClickInside, children, className, - isVisible, + isVisible: isVisibleProp, onAfterClose, onClose, anchorOrigin: anchorOriginProp = defaultAnchorOrigin, @@ -210,6 +211,9 @@ export const Popup = forwardRef(function Popup( fixed } = props const { spring, shadows } = useTheme() + const [popupState, setPopupState] = useState('closed') + + const isVisible = popupState !== 'closed' const handleClose = useCallback(() => { onClose?.() @@ -344,7 +348,15 @@ export const Popup = forwardRef(function Popup( return () => {} }, [isVisible, handleClose]) - const transitions = useTransition(isVisible, null, { + useEffect(() => { + if (popupState === 'closed' && isVisibleProp) { + setPopupState('opening') + } else if (popupState === 'open' && !isVisibleProp) { + setPopupState('closing') + } + }, [isVisibleProp, popupState]) + + const transitions = useTransition(isVisibleProp, null, { from: { transform: `scale(0)`, opacity: 0 @@ -358,7 +370,10 @@ export const Popup = forwardRef(function Popup( opacity: 0 }, config: spring.standard, - unique: true + unique: true, + onDestroyed: (isDestroyed) => { + setPopupState(isDestroyed ? 'closed' : 'open') + } }) const rootStyle = { zIndex, position: fixed ? ('fixed' as const) : undefined } @@ -372,49 +387,51 @@ export const Popup = forwardRef(function Popup( return ( <> {/* Portal the popup out of the dom structure so that it has a separate stacking context */} - {ReactDOM.createPortal( -
- {transitions.map(({ item, key, props }) => - item ? ( - - {showHeader && ( -
+ {transitions.map(({ item, key, props }) => + item ? ( + - {hideCloseButton ? null : ( - + {showHeader && ( +
+ {hideCloseButton ? null : ( + + )} +
{title}
+
)} -
{title}
-
- )} - {children} -
- ) : null - )} -
, - portalLocation - )} + {children} + + ) : null + )} + , + portalLocation + ) + : null} ) }) diff --git a/packages/harmony/src/components/modal/Modal.tsx b/packages/harmony/src/components/modal/Modal.tsx index babff7bf90b..15008a81db8 100644 --- a/packages/harmony/src/components/modal/Modal.tsx +++ b/packages/harmony/src/components/modal/Modal.tsx @@ -17,6 +17,8 @@ import ReactDOM from 'react-dom' import { animated, useTransition } from 'react-spring' import { useEffectOnce } from 'react-use' +import { ModalState } from 'utils/modalState' + import { useHotkeys, useScrollLock, useClickOutside } from '../../hooks' import { IconClose } from '../../icons' @@ -47,7 +49,7 @@ const getOffset = (anchor: Anchor, verticalAnchorOffset: number) => { return { [anchorPropertyMap[anchor]]: verticalAnchorOffset } } -const useModalRoot = (id: string, zIndex?: number) => { +const useModalRoot = (id: string, isOpen: boolean, zIndex?: number) => { const [modalRoot, setModalRoot] = useState(null) const [modalBg, setModalBg] = useState(null) @@ -59,6 +61,21 @@ const useModalRoot = (id: string, zIndex?: number) => { let bgEl = document.getElementById(uniqueBgId) let container = document.getElementById(uniqueRootContainerId) + if (!isOpen) { + if (bgEl) { + document.body.removeChild(bgEl) + } + if (el && container) { + container.removeChild(el) + } + + if (container) { + document.body.removeChild(container) + } + + return + } + if (!bgEl) { bgEl = document.createElement('div') bgEl.id = uniqueBgId @@ -88,7 +105,7 @@ const useModalRoot = (id: string, zIndex?: number) => { setModalRoot(el) setModalBg(bgEl) - }, [id, zIndex]) + }, [id, zIndex, isOpen]) return [modalRoot, modalBg] } @@ -101,7 +118,7 @@ export const Modal = forwardRef(function Modal( children, onClose, onClosed, - isOpen, + isOpen: isOpenProp, wrapperClassName, bodyClassName, titleClassName, @@ -147,11 +164,13 @@ export const Modal = forwardRef(function Modal( } }) + const [modalState, setModalState] = useState('closed') + const isOpen = modalState !== 'closed' + const isDoneOpening = modalState === 'open' const { spring } = useTheme() const id = useMemo(() => modalKey || uniqueId('modal-'), [modalKey]) const titleId = ariaLabelledbyProp || `${id}-title` const subtitleId = ariaDescribedbyProp || `${id}-subtitle` - const [isDoneOpening, setIsDoneOpening] = useState(false) const modalContextValue = useMemo(() => { return { titleId, subtitleId, onClose, isDoneOpening } }, [titleId, subtitleId, onClose, isDoneOpening]) @@ -163,14 +182,17 @@ export const Modal = forwardRef(function Modal( [allowScroll] ) - const [modalRoot, bgModal] = useModalRoot(id, zIndex) - const [isDestroyed, setIsDestroyed] = useState(isOpen) + const [modalRoot, bgModal] = useModalRoot(id, isOpen, zIndex) const { incrementScrollCount, decrementScrollCount } = useModalScrollCount() - useScrollLock(isDestroyed, incrementScrollCount, decrementScrollCount) + useScrollLock(isOpen, incrementScrollCount, decrementScrollCount) useEffect(() => { - if (isOpen) setIsDestroyed(true) - }, [isOpen]) + if (modalState === 'closed' && isOpenProp) { + setModalState('opening') + } else if (modalState === 'open' && !isOpenProp) { + setModalState('closing') + } + }, [isOpenProp, modalState]) useEffect(() => { if (isOpen) { @@ -191,29 +213,23 @@ export const Modal = forwardRef(function Modal( return () => {} }, [isOpen, bgModal, onTouchMove, modalRoot]) - const transition = useTransition(isOpen, null, { + const transition = useTransition(isOpenProp, null, { from: { transform: 'scale(0)', opacity: 0 }, // @ts-ignore function is a valid value for enter, but the types don't acknowledge that enter: - (item: boolean) => + () => async ( next: (props: { transform: string; opacity: number }) => Promise ) => { await next({ transform: 'scale(1)', opacity: 1 }) - if (item) { - setImmediate(() => setIsDoneOpening(true)) - } }, leave: { transform: 'scale(0)', opacity: 0 }, unique: true, config: spring.standard, - onDestroyed: () => { - if (!isOpen) { - setIsDestroyed(false) - setIsDoneOpening(false) - if (onClosed) { - onClosed() - } + onDestroyed: (isDestroyed) => { + setModalState(isDestroyed ? 'closed' : 'open') + if (isDestroyed) { + onClosed?.() } } }) @@ -309,72 +325,77 @@ export const Modal = forwardRef(function Modal( const handleModalContentClicked: MouseEventHandler = () => { modalContentClickedRef.current = true } + return ( <> - {modalRoot && - ReactDOM.createPortal( - <> - {transition.map( - ({ item, props, key }) => - item && ( - + {modalRoot && isOpen + ? ReactDOM.createPortal( + <> + {transition.map( + ({ item, props, key }) => + item && ( - <> - {/** Begin @deprecated section (moved to ModalHeader and ModalTitle sub-components) */} - {showTitleHeader && ( -
- {showDismissButton && ( + + <> + {/** Begin @deprecated section (moved to ModalHeader and ModalTitle sub-components) */} + {showTitleHeader && ( +
+ {showDismissButton && ( +
+ +
+ )}
- + {title} +
+
+ {subtitle}
- )} -
- {title} -
-
- {subtitle}
-
- )} - {/** End @deprecated section */} - - {children} - - + )} + {/** End @deprecated section */} + + {children} + + +
- - ) - )} - , - modalRoot - )} + ) + )} + , + modalRoot + ) + : null} ) }) diff --git a/packages/harmony/src/components/popup-menu/PopupMenu.module.css b/packages/harmony/src/components/popup-menu/PopupMenu.module.css index 495b9ec4760..ae9d9673bf1 100644 --- a/packages/harmony/src/components/popup-menu/PopupMenu.module.css +++ b/packages/harmony/src/components/popup-menu/PopupMenu.module.css @@ -18,7 +18,7 @@ color: var(--harmony-neutral); } -.item .icon { +.icon { display: flex; align-items: center; justify-content: center; @@ -32,10 +32,10 @@ .item:hover { cursor: pointer; - color: var(--harmony-white); + color: var(--harmony-static-white); background: var(--harmony-secondary); } .item:hover path { - fill: var(--harmony-white); + fill: var(--harmony-static-white); } diff --git a/packages/harmony/src/components/popup-menu/PopupMenu.tsx b/packages/harmony/src/components/popup-menu/PopupMenu.tsx index 62051176c59..67d30a7ca49 100644 --- a/packages/harmony/src/components/popup-menu/PopupMenu.tsx +++ b/packages/harmony/src/components/popup-menu/PopupMenu.tsx @@ -106,11 +106,11 @@ export const PopupMenu = forwardRef( onClick={handleMenuItemClick(item)} tabIndex={i === 0 ? 0 : -1} > - {item.icon && ( + {item.icon ? (
{item.icon}
- )} + ) : null} {item.text} ))} diff --git a/packages/harmony/src/utils/modalState.ts b/packages/harmony/src/utils/modalState.ts new file mode 100644 index 00000000000..e22fc31f093 --- /dev/null +++ b/packages/harmony/src/utils/modalState.ts @@ -0,0 +1 @@ +export type ModalState = 'opening' | 'open' | 'closing' | 'closed' diff --git a/packages/identity-service/package.json b/packages/identity-service/package.json index 5cfc536ed10..ba5ae0163cf 100644 --- a/packages/identity-service/package.json +++ b/packages/identity-service/package.json @@ -151,5 +151,5 @@ "src/notifications/renderEmail/*" ] }, - "version": "0.0.27" + "version": "0.0.28" } diff --git a/packages/libs/package.json b/packages/libs/package.json index 83f29d505e5..30344c04bfc 100644 --- a/packages/libs/package.json +++ b/packages/libs/package.json @@ -1,6 +1,6 @@ { "name": "@audius/sdk", - "version": "3.0.40", + "version": "3.0.41", "audius": { "releaseSHA": "f1d70a2a0643c5c84d8ab053f70c1e0a2ec3ad49" }, @@ -63,9 +63,9 @@ "prepack": "turbo run build" }, "dependencies": { - "@audius/fixed-decimal": "^0.0.21", + "@audius/fixed-decimal": "*", "@audius/hedgehog": "3.0.0-alpha.0", - "@audius/spl": "^0.0.27", + "@audius/spl": "*", "@babel/core": "^7.23.7", "@babel/plugin-proposal-class-static-block": "7.21.0", "@babel/runtime": "7.18.3", diff --git a/packages/libs/src/api/Track.ts b/packages/libs/src/api/Track.ts index 2b42cc76bf6..d3a3341d487 100644 --- a/packages/libs/src/api/Track.ts +++ b/packages/libs/src/api/Track.ts @@ -603,51 +603,105 @@ export class Track extends Base { * Prevents additional fields from being included in metadata */ static _parseTrackMetadata(trackMetadata: TrackMetadata): TrackMetadata { + const { + blocknumber, + is_delete, + track_id, + is_downloadable, + is_original_available, + created_at, + isrc, + iswc, + credits_splits, + description, + genre, + has_current_user_reposted, + has_current_user_saved, + license, + mood, + play_count, + owner_id, + release_date, + repost_count, + save_count, + tags, + title, + track_segments, + cover_art, + cover_art_sizes, + is_unlisted, + is_available, + is_stream_gated, + stream_conditions, + is_download_gated, + download_conditions, + permalink, + preview_start_seconds, + duration, + is_invalid, + stem_of, + ddex_app, + is_playlist_upload, + audio_upload_id, + track_cid, + orig_file_cid, + orig_filename, + preview_cid, + dateListened, + remix_of, + ai_attribution_user_id, + ...other + } = trackMetadata + if (typeof other !== 'undefined') { + console.warn('Unknown keys in upload metadata:', Object.keys(other)) + } return { - blocknumber: trackMetadata.blocknumber, - is_delete: trackMetadata.is_delete, - track_id: trackMetadata.track_id, - is_downloadable: trackMetadata.is_downloadable, - is_original_available: trackMetadata.is_original_available, - created_at: trackMetadata.created_at, - isrc: trackMetadata.isrc, - iswc: trackMetadata.iswc, - credits_splits: trackMetadata.credits_splits, - description: trackMetadata.description, - genre: trackMetadata.genre, - has_current_user_reposted: trackMetadata.has_current_user_reposted, - has_current_user_saved: trackMetadata.has_current_user_saved, - license: trackMetadata.license, - mood: trackMetadata.mood, - play_count: trackMetadata.play_count, - owner_id: trackMetadata.owner_id, - release_date: trackMetadata.release_date, - repost_count: trackMetadata.repost_count, - save_count: trackMetadata.save_count, - tags: trackMetadata.tags, - title: trackMetadata.title, - track_segments: trackMetadata.track_segments, - cover_art: trackMetadata.cover_art, - cover_art_sizes: trackMetadata.cover_art_sizes, - is_unlisted: trackMetadata.is_unlisted, - is_available: trackMetadata.is_available, - is_stream_gated: trackMetadata.is_stream_gated, - stream_conditions: trackMetadata.stream_conditions, - is_download_gated: trackMetadata.is_download_gated, - download_conditions: trackMetadata.download_conditions, - permalink: trackMetadata.permalink, - preview_start_seconds: trackMetadata.preview_start_seconds, - duration: trackMetadata.duration, - is_invalid: trackMetadata.is_invalid, - stem_of: trackMetadata.stem_of, - ddex_app: trackMetadata.ddex_app, - is_playlist_upload: trackMetadata.is_playlist_upload, - audio_upload_id: trackMetadata.audio_upload_id, - track_cid: trackMetadata.track_cid, - orig_file_cid: trackMetadata.orig_file_cid, - orig_filename: trackMetadata.orig_filename, - preview_cid: trackMetadata.preview_cid, - dateListened: trackMetadata.dateListened + blocknumber, + is_delete, + track_id, + is_downloadable, + is_original_available, + created_at, + isrc, + iswc, + credits_splits, + description, + genre, + has_current_user_reposted, + has_current_user_saved, + license, + mood, + play_count, + owner_id, + release_date, + repost_count, + save_count, + tags, + title, + track_segments, + cover_art, + cover_art_sizes, + is_unlisted, + is_available, + is_stream_gated, + stream_conditions, + is_download_gated, + download_conditions, + permalink, + preview_start_seconds, + duration, + is_invalid, + stem_of, + ddex_app, + is_playlist_upload, + audio_upload_id, + track_cid, + orig_file_cid, + orig_filename, + preview_cid, + dateListened, + remix_of, + ai_attribution_user_id } } } diff --git a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts index 4265317e30f..bd487541e04 100644 --- a/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts +++ b/packages/libs/src/sdk/services/DiscoveryNodeSelector/DiscoveryNodeSelector.ts @@ -738,7 +738,7 @@ export class DiscoveryNodeSelector implements DiscoveryNodeSelectorService { // Retry once on new endpoint return await context.fetch(`${newEndpoint}${path}`, context.init) } catch (e) { - this.logger.error('Retry on new node failed', newEndpoint) + this.logger.error('Retry on new node failed', newEndpoint, e) } } return undefined diff --git a/packages/libs/src/utils/types.ts b/packages/libs/src/utils/types.ts index f135641f586..7bb45e48ccc 100644 --- a/packages/libs/src/utils/types.ts +++ b/packages/libs/src/utils/types.ts @@ -186,6 +186,11 @@ export type TrackMetadata = { copyright_line?: Nullable producer_copyright_line?: Nullable parental_warning_type?: Nullable + remix_of: Nullable<{ + tracks: Array<{ + parent_track_id: ID + }> + }> // Optional Fields is_invalid?: boolean diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle index cf9b9daa9c1..1f0f699e6ef 100755 --- a/packages/mobile/android/app/build.gradle +++ b/packages/mobile/android/app/build.gradle @@ -113,7 +113,7 @@ android { // versionCode is automatically incremented in CI versionCode 1 // Make sure this is above the currently released Android version in the play store if your changes touch native code: - versionName "1.1.417" + versionName "1.1.418" resValue "string", "build_config_package", "co.audius.app" resValue 'string', "CODE_PUSH_APK_BUILD_TIME", String.format("\"%d\"", System.currentTimeMillis()) resConfigs "en" diff --git a/packages/mobile/ios/AudiusReactNative/Info.plist b/packages/mobile/ios/AudiusReactNative/Info.plist index ce9740a1239..e688e34442e 100644 --- a/packages/mobile/ios/AudiusReactNative/Info.plist +++ b/packages/mobile/ios/AudiusReactNative/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.1.91 + 1.1.92 CFBundleSignature ???? CFBundleURLTypes diff --git a/packages/mobile/package.json b/packages/mobile/package.json index fb65cc4c977..de6aa9e9ef4 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "audius-mobile-client", - "version": "1.5.72", + "version": "1.5.73", "private": true, "scripts": { "android:dev": "ENVFILE=.env.dev turbo run android -- --mode=prodDebug", diff --git a/packages/mobile/src/screens/search-screen/SearchScreen.tsx b/packages/mobile/src/screens/search-screen/SearchScreen.tsx index 615e1e6ab4a..59c0dd8d8b9 100644 --- a/packages/mobile/src/screens/search-screen/SearchScreen.tsx +++ b/packages/mobile/src/screens/search-screen/SearchScreen.tsx @@ -40,7 +40,7 @@ export const SearchScreen = () => { ) const renderSearchContent = () => { - if (hasResults) { + if (hasResults && !searchBarText.startsWith('#')) { return } if (searchBarText && !hasResults) { diff --git a/packages/mobile/src/screens/sign-on-screen/screens/CreateEmailScreen.tsx b/packages/mobile/src/screens/sign-on-screen/screens/CreateEmailScreen.tsx index 43ea3264f43..0451eecbc95 100644 --- a/packages/mobile/src/screens/sign-on-screen/screens/CreateEmailScreen.tsx +++ b/packages/mobile/src/screens/sign-on-screen/screens/CreateEmailScreen.tsx @@ -106,17 +106,7 @@ export const CreateEmailScreen = (props: SignOnScreenProps) => { ) : null} - - {createEmailPageMessages.subHeader.line1} - {'\n'} - {createEmailPageMessages.subHeader.line2} - - } - centered - /> + { - beforeEach(() => { - localStorage.setItem('HAS_REQUESTED_BROWSER_PUSH_PERMISSION', 'true') - }) - - it('should favorite and unfavorite track correctly', () => { - const base64Entropy = Buffer.from(user.entropy).toString('base64') - cy.visit(`${track.route}?login=${base64Entropy}`) - - cy.findByText(user.name, { - timeout: Cypress.env('initialLoadTimeout') - }).should('exist') - cy.findByRole('heading', { name: track.name, timeout: 20000 }).should( - 'exist' - ) - - cy.findByRole('group', { name: /track actions/i }).within(() => { - cy.findByRole('button', { name: /favorite$/i }).click() - cy.findByRole('button', { name: /favorited/i }).should('exist') - }) - waitForTransaction(1) - cy.reload() - - cy.findByRole('group', { name: /track actions/i }).within(() => { - cy.findByRole('button', { name: /favorited/i }).click() - cy.findByRole('button', { name: /favorite$/i }).should('exist') - }) - - waitForTransaction(2) - cy.reload() - - cy.findByRole('group', { name: /track actions/i }).within(() => { - cy.findByRole('button', { name: /favorite$/i }).should('exist') - }) - }) -}) diff --git a/packages/probers/cypress/e2e/flaky test meme.jpeg b/packages/probers/cypress/e2e/flaky test meme.jpeg deleted file mode 100644 index ac7f4e40f97..00000000000 Binary files a/packages/probers/cypress/e2e/flaky test meme.jpeg and /dev/null differ diff --git a/packages/probers/cypress/e2e/flaky-test-jail.md b/packages/probers/cypress/e2e/flaky-test-jail.md deleted file mode 100644 index be3ebc54850..00000000000 --- a/packages/probers/cypress/e2e/flaky-test-jail.md +++ /dev/null @@ -1,25 +0,0 @@ -# Flaky Test Jail - -![flaky test meme](./flaky%20test%20meme.jpeg) - -A list of flaky/misbehaving tests that we are currently skipping in order to keep a green baseline - -## Current Inmates - [C-3508] - -#### [Sign Up](./signUp.cy.ts) - [C-3593] - -"should create an account" tests are skipped until they can be updated - - - -## Test Jail Process - -- Create an appropriate ticket for addressing the test -- Update this doc with the flaky tests and a description -- Add a TODO: comment by the test with the ticket number for searchability diff --git a/packages/probers/cypress/e2e/playTrack.cy.ts b/packages/probers/cypress/e2e/playTrack.cy.ts deleted file mode 100644 index 25727c87505..00000000000 --- a/packages/probers/cypress/e2e/playTrack.cy.ts +++ /dev/null @@ -1,22 +0,0 @@ -describe('Play Track', () => { - it('should play a trending track', () => { - cy.visit('trending') - cy.findByRole('list', { name: /weekly trending tracks/i }).within(() => { - // Wait for skeletons to not exist to indicate we are done loading - cy.get('[aria-busy]').should('not.exist') - - cy.findAllByRole('listitem').first().click('left') - }) - - cy.findByRole('button', { - name: /pause track/i, - timeout: 20000 - }).should('exist') - cy.window().its('audio.paused').should('equal', false) - cy.findByRole('button', { - name: /pause track/i - }).click() - cy.findByRole('button', { name: /play track/i }).should('exist') - cy.window().its('audio.paused').should('equal', true) - }) -}) diff --git a/packages/probers/cypress/e2e/repostTrack.cy.ts b/packages/probers/cypress/e2e/repostTrack.cy.ts deleted file mode 100644 index 360ed815e54..00000000000 --- a/packages/probers/cypress/e2e/repostTrack.cy.ts +++ /dev/null @@ -1,41 +0,0 @@ -import track from '../fixtures/track.json' -import user from '../fixtures/user.json' - -import { waitForTransaction } from './uilts' - -describe('Repost Track', () => { - beforeEach(() => { - localStorage.setItem('HAS_REQUESTED_BROWSER_PUSH_PERMISSION', 'true') - }) - - it('should repost and unrepost track correctly', () => { - const base64Entropy = Buffer.from(user.entropy).toString('base64') - cy.visit(`${track.route}?login=${base64Entropy}`) - - cy.findByText(user.name, { - timeout: Cypress.env('initialLoadTimeout') - }).should('exist') - cy.findByRole('heading', { name: track.name, timeout: 20000 }).should( - 'exist' - ) - - cy.findByRole('group', { name: /track actions/i }).within(() => { - cy.findByRole('button', { name: /repost$/i }).click() - cy.findByRole('button', { name: /reposted/i }).should('exist') - }) - waitForTransaction(1) - cy.reload() - - cy.findByRole('group', { name: /track actions/i }).within(() => { - cy.findByRole('button', { name: /reposted/i }).click() - cy.findByRole('button', { name: /repost$/i }).should('exist') - }) - - waitForTransaction(2) - cy.reload() - - cy.findByRole('group', { name: /track actions/i }).within(() => { - cy.findByRole('button', { name: /repost$/i }).should('exist') - }) - }) -}) diff --git a/packages/probers/cypress/e2e/signIn.cy.ts b/packages/probers/cypress/e2e/signIn.cy.ts deleted file mode 100644 index f80c96f12ab..00000000000 --- a/packages/probers/cypress/e2e/signIn.cy.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { email, password, name, handle } from '../fixtures/user.json' - -function assertOnSignInPage() { - cy.findByRole('heading', { name: /sign into audius/i, level: 1 }).should( - 'exist' - ) -} - -describe('Sign In', () => { - beforeEach(() => { - localStorage.setItem('FeatureFlagOverride:sign_up_redesign', 'enabled') - }) - - it('can navigate to sign-in from trending screen', () => { - cy.visit('trending') - cy.findByText(/have an account\?/i).should('exist') - cy.findByRole('link', { name: /sign in/i }).click() - assertOnSignInPage() - }) - - it('/signin goes to sign-in', () => { - cy.visit('signin') - assertOnSignInPage() - }) - - it('can navigate to sign-in from sign-up', () => { - cy.visit('signup') - cy.findByText(/already have an account?/i) - cy.findByRole('link', { name: /Sign In/ }).click() - assertOnSignInPage() - }) - - it('can navigate to sign-in after entering email in sign-up', () => { - cy.visit('signup') - cy.findByRole('textbox', { name: /email/i }).type(email) - cy.findByRole('button', { name: /sign up free/i }).click() - cy.findByRole('alert').within(() => { - cy.findByRole('link', { name: /Sign In/ }).click() - }) - assertOnSignInPage() - }) - - // We need to integrate a hard-coded otp for this user - it.skip('can sign in', () => { - cy.visit('signin') - assertOnSignInPage() - cy.findByRole('textbox', { name: /email/i }).type(email) - // Password inputs dont have a role, so we just check against label text - // https://github.com/testing-library/dom-testing-library/issues/567#issue-616906804 - cy.findByLabelText(/^password/i).type(password) - cy.findByRole('button', { name: /sign in/i }).click() - - cy.findByRole('heading', { - name: /your feed/i, - level: 1, - timeout: 20000 - }).should('exist') - - cy.findByText(name).should('exist') - cy.findByText(`@${handle}`).should('exist') - }) -}) diff --git a/packages/probers/cypress/e2e/signOut.cy.ts b/packages/probers/cypress/e2e/signOut.cy.ts deleted file mode 100644 index abb8591440d..00000000000 --- a/packages/probers/cypress/e2e/signOut.cy.ts +++ /dev/null @@ -1,24 +0,0 @@ -import user from '../fixtures/user.json' - -describe('Sign Out', () => { - beforeEach(() => { - localStorage.setItem('HAS_REQUESTED_BROWSER_PUSH_PERMISSION', 'true') - }) - - it('should be able to sign out', () => { - const base64Entropy = Buffer.from(user.entropy).toString('base64') - cy.visit(`trending?login=${base64Entropy}`) - cy.findByText(user.name).should('exist') - - cy.visit('settings') - - cy.findByRole('heading', { name: /settings/i, level: 1 }).should('exist') - cy.findByRole('button', { name: /sign out/i }).click() - - cy.findByRole('dialog', { name: /hold up/i }).within(() => { - cy.findByRole('button', { name: /sign out/i }).click() - }) - - cy.findByText(/have an account?/i, { timeout: 20000 }).should('exist') - }) -}) diff --git a/packages/probers/cypress/e2e/signUp.cy.ts b/packages/probers/cypress/e2e/signUp.cy.ts deleted file mode 100644 index 45f4be465f6..00000000000 --- a/packages/probers/cypress/e2e/signUp.cy.ts +++ /dev/null @@ -1,206 +0,0 @@ -/* eslint-disable jest/no-identical-title */ -import dayjs from 'dayjs' - -function generateTestUser() { - const ts = dayjs().format('YYMMDD_HHmmss') - const email = `prober+${ts}@audius.co` - const password = 'Pa$$w0rdTest' - const name = `Prober ${ts}` - const handle = `p_${ts}` - return { - email, - password, - name, - handle - } -} - -type User = { - email: string -} - -function assertOnSignUpPage() { - cy.findByRole('heading', { name: /sign up for audius/i, level: 1 }).should( - 'exist' - ) -} - -function assertOnCreatePasswordPage(user: User) { - cy.findByRole('heading', { name: /create your password/i }).should('exist') - - cy.findByText( - /Create a password that’s secure and easy to remember!/i - ).should('exist') - - cy.findByText(/your email/i).should('exist') - cy.findByText(user.email).should('exist') -} - -function testSignUp({ isMobile = false }) { - const testUser = generateTestUser() - const { email, password, handle, name } = testUser - cy.visit('signup') - cy.findByRole('textbox', { name: /email/i }).type(email) - cy.findByRole('button', { name: /sign up free/i }).click() - - assertOnCreatePasswordPage(testUser) - - // Password inputs dont have a role, so we just check against label text - // https://github.com/testing-library/dom-testing-library/issues/567#issue-616906804 - cy.findByLabelText(/^password/i).type(password) - cy.findByLabelText(/confirm password/i).type(password, { force: true }) - - cy.findByRole('button', { name: /continue/i }).click() - - cy.findByRole('heading', { name: /pick your handle/i }).should('exist') - cy.findByText(/this is how others find and tag you/i).should('exist') - cy.findByRole('textbox', { name: /handle/i }).type(handle) - cy.findByRole('button', { name: /continue/i }).click() - - cy.findByRole('heading', { name: /finish your profile/i }).should('exist') - cy.findByText(/your photos & display name is how others see you./i).should( - 'exist' - ) - - cy.findByTestId('coverPhoto-dropzone').attachFile('cover-photo.jpeg', { - subjectType: 'drag-n-drop' - }) - - cy.findByTestId('profileImage-dropzone').attachFile('profile-picture.jpeg', { - subjectType: 'drag-n-drop' - }) - - cy.findByRole('textbox', { name: /display name/i }).type(name) - cy.findByRole('button', { name: /continue/i }).click() - - cy.findByRole('heading', { name: /select your genres/i }).should('exist') - cy.findByText(/start by picking some of your favorite genres./i).should( - 'exist' - ) - - cy.findByText(name).should('exist') - cy.findByText(`@${handle}`).should('exist') - - const genres = [/^acoustic/i, /^pop/i, /^lo-fi/i, /^electronic/i] - - for (const genre of genres) { - cy.findByRole('checkbox', { name: genre }).check() - } - - cy.findByRole('button', { name: /continue/i }).click() - - cy.findByRole('heading', { name: /follow at least 3 artists/i }).should( - 'exist' - ) - cy.findByText(/curate your feed with tracks uploaded/i).should('exist') - - cy.findByRole('radiogroup', { name: /genre/i }).within(() => { - cy.findByRole('radio', { name: /featured/i }).should('be.checked') - - for (const genre of genres) { - cy.findByRole('radio', { name: genre }).should('exist') - } - }) - - function selectArtist(sectionName: RegExp) { - cy.findByRole('group', { name: sectionName }).within(() => { - cy.findAllByRole('checkbox', { checked: false }).first().click() - }) - } - - selectArtist(/pick featured artists/i) - - cy.findByRole('radio', { name: /acoustic/i }).click() - selectArtist(/pick acoustic artists/i) - - cy.findByRole('radio', { name: /electronic/i }).click() - selectArtist(/pick electronic artists/i) - - cy.findByRole('button', { name: /continue/i }).click() - - if (!isMobile) { - cy.findByRole('heading', { name: /get the app/i }).click() - cy.findByText(/take audius with you/i).should('exist') - cy.findByRole('button', { name: /continue/i }).click() - } - - cy.findByRole('dialog', { - name: /welcome to audius/i, - timeout: 120000 - }).within(() => { - cy.findByRole('button', { name: /start listening/i }).click() - }) -} - -describe('Sign Up', () => { - beforeEach(() => { - localStorage.setItem('FeatureFlagOverride:sign_up_redesign', 'enabled') - }) - - context('desktop', () => { - it('can navigate to signup from trending', () => { - cy.visit('trending') - cy.findByText(/have an account\?/i).should('exist') - cy.findByRole('link', { name: /sign up/i }).click() - assertOnSignUpPage() - }) - - it('/signup goes to sign-up', () => { - cy.visit('signup') - assertOnSignUpPage() - }) - - it('can navigate to sign-up from sign-in', () => { - cy.visit('signin') - cy.findByRole('link', { name: /create an account/i }).click() - - assertOnSignUpPage() - }) - - it('can navigate to sign-up from the public site', () => { - cy.visit('') - cy.findByRole('link', { name: /sign up/i }).click() - assertOnSignUpPage() - }) - - it('should create an account', () => { - testSignUp({ isMobile: false }) - }) - }) - - context('mobile', () => { - beforeEach(() => { - cy.viewport('iphone-x') - }) - - it('can navigate to signup from trending', () => { - cy.visit('trending') - cy.findByRole('link', { name: /sign up/i }).click() - assertOnSignUpPage() - }) - - it('/signup goes to sign-up', () => { - cy.visit('signup') - assertOnSignUpPage() - }) - - it('can navigate to sign-up from sign-in', () => { - cy.visit('signin') - cy.findByRole('link', { name: /create an account/i }).click() - - assertOnSignUpPage() - }) - - it('can navigate to sign-up from the public site', () => { - cy.visit('') - cy.findByRole('button', { name: /open nav menu/i }).click() - cy.findByRole('link', { name: /sign up/i }).click() - - assertOnSignUpPage() - }) - - it('should create an account', () => { - testSignUp({ isMobile: true }) - }) - }) -}) diff --git a/packages/probers/cypress/e2e/smoke/album.cy.ts b/packages/probers/cypress/e2e/smoke/album.cy.ts deleted file mode 100644 index db3859b0c84..00000000000 --- a/packages/probers/cypress/e2e/smoke/album.cy.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { route, name } from '../../fixtures/album.json' - -describe('Smoke test -- album page', () => { - it('Should load an album page when visited!', () => { - cy.visit(route) - cy.findByRole('heading', { name, level: 1, timeout: 10000 }).should('exist') - }) -}) diff --git a/packages/probers/cypress/e2e/smoke/feed.cy.ts b/packages/probers/cypress/e2e/smoke/feed.cy.ts deleted file mode 100644 index 4bf7b92ccb6..00000000000 --- a/packages/probers/cypress/e2e/smoke/feed.cy.ts +++ /dev/null @@ -1,20 +0,0 @@ -import user from '../../fixtures/user.json' - -describe('Smoke test -- feed page', () => { - beforeEach(() => { - localStorage.setItem('HAS_REQUESTED_BROWSER_PUSH_PERMISSION', 'true') - }) - - it('should load feed page when visited', () => { - const base64Entropy = Buffer.from(user.entropy).toString('base64') - cy.visit(`trending?login=${base64Entropy}`) - - cy.findByText(user.name, { - timeout: Cypress.env('initialLoadTimeout') - }).should('exist') - - cy.visit('feed') - - cy.findByRole('heading', { name: /your feed/i, level: 1 }).should('exist') - }) -}) diff --git a/packages/probers/cypress/e2e/smoke/playlist.cy.ts b/packages/probers/cypress/e2e/smoke/playlist.cy.ts deleted file mode 100644 index db085793410..00000000000 --- a/packages/probers/cypress/e2e/smoke/playlist.cy.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { route, name } from '../../fixtures/playlist.json' - -describe('Smoke test -- playlist page', () => { - it('should load a playlist page when visited', () => { - cy.visit(route) - cy.findByRole('heading', { name, level: 1, timeout: 20000 }).should('exist') - }) -}) diff --git a/packages/probers/cypress/e2e/smoke/remix.cy.ts b/packages/probers/cypress/e2e/smoke/remix.cy.ts deleted file mode 100644 index e9d7a6b5f99..00000000000 --- a/packages/probers/cypress/e2e/smoke/remix.cy.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { route, name } from '../../fixtures/remix.json' - -describe('Smoke test -- remix page', () => { - it('should load a remix page when visited', () => { - cy.visit(route) - cy.findByRole('heading', { name, level: 1 }).should('exist') - }) -}) diff --git a/packages/probers/cypress/e2e/smoke/remixes.cy.ts b/packages/probers/cypress/e2e/smoke/remixes.cy.ts deleted file mode 100644 index 0c0f721f1fc..00000000000 --- a/packages/probers/cypress/e2e/smoke/remixes.cy.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { route } from '../../fixtures/remixes.json' - -describe('Smoke test -- remixes page', () => { - it('should load a remixes page when visited', () => { - cy.visit(route) - cy.findByRole('heading', { - name: 'Remixes', - level: 1, - timeout: 10000 - }).should('exist') - }) -}) diff --git a/packages/probers/cypress/e2e/smoke/track.cy.ts b/packages/probers/cypress/e2e/smoke/track.cy.ts deleted file mode 100644 index 29071fa2dc4..00000000000 --- a/packages/probers/cypress/e2e/smoke/track.cy.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { route, name } from '../../fixtures/track.json' - -describe('Smoke test -- track page', () => { - it('should load a track page when visited', () => { - cy.visit(route) - cy.findByRole('heading', { name, level: 1 }).should('exist') - }) -}) diff --git a/packages/probers/cypress/e2e/smoke/trending.cy.ts b/packages/probers/cypress/e2e/smoke/trending.cy.ts deleted file mode 100644 index b2feb220c0d..00000000000 --- a/packages/probers/cypress/e2e/smoke/trending.cy.ts +++ /dev/null @@ -1,6 +0,0 @@ -describe('Smoke test -- trending page', () => { - it('should load trending page when visited', () => { - cy.visit('trending') - cy.findByRole('heading', { name: /trending/i, level: 1 }).should('exist') - }) -}) diff --git a/packages/probers/cypress/e2e/smoke/upload.cy.ts b/packages/probers/cypress/e2e/smoke/upload.cy.ts deleted file mode 100644 index 55e8471669b..00000000000 --- a/packages/probers/cypress/e2e/smoke/upload.cy.ts +++ /dev/null @@ -1,21 +0,0 @@ -import user from '../../fixtures/user.json' -describe('Smoke test -- upload page', () => { - beforeEach(() => { - localStorage.setItem('HAS_REQUESTED_BROWSER_PUSH_PERMISSION', 'true') - }) - - it('should load upload page when visited', () => { - const base64Entropy = Buffer.from(user.entropy).toString('base64') - cy.visit(`trending?login=${base64Entropy}`) - - cy.findByText(user.name, { - timeout: Cypress.env('initialLoadTimeout') - }).should('exist') - - cy.visit('upload') - - cy.findByRole('heading', { name: /upload your music/i, level: 1 }).should( - 'exist' - ) - }) -}) diff --git a/packages/probers/cypress/e2e/uilts.ts b/packages/probers/cypress/e2e/uilts.ts deleted file mode 100644 index 0d3433de167..00000000000 --- a/packages/probers/cypress/e2e/uilts.ts +++ /dev/null @@ -1,24 +0,0 @@ -export const waitForTransaction = (transactionNumber: number) => { - cy.intercept({ method: 'POST', url: '**/relay' }).as( - `relayCheck-${transactionNumber}` - ) - cy.wait(`@relayCheck-${transactionNumber}`).then((xhr) => { - const { blockHash, blockNumber } = xhr.response.body.receipt - cy.intercept({ - url: '**/block_confirmation*', - query: { blockhash: blockHash, blocknumber: String(blockNumber) } - }).as(`blockConfirmation-${blockNumber}`) - - waitForBlockConfirmation(`@blockConfirmation-${blockNumber}`) - }) -} - -const waitForBlockConfirmation = (routeAlias, retries = 3) => { - cy.wait(routeAlias, { timeout: 10000 }).then((xhr) => { - const { block_passed } = xhr.response.body.data - if (block_passed) { - } else if (retries > 0) waitForBlockConfirmation(routeAlias, retries - 1) - // wait for the next response - else throw new Error('All requests returned non-200 response') - }) -} diff --git a/packages/probers/cypress/e2e/uploadCollection.cy.ts b/packages/probers/cypress/e2e/uploadCollection.cy.ts deleted file mode 100644 index c40dfd3fb91..00000000000 --- a/packages/probers/cypress/e2e/uploadCollection.cy.ts +++ /dev/null @@ -1,206 +0,0 @@ -import dayjs from 'dayjs' - -import aiAttribution from '../fixtures/aiAttribution.json' -import remix from '../fixtures/remix.json' -const timestamp = dayjs().format('YYMMDD_HHmmss') - -const visitUpload = () => { - cy.login() - cy.findByRole('link', { name: /upload track/i }).click() - cy.findByRole('heading', { name: /upload your music/i, level: 1 }).should( - 'exist' - ) -} - -const completeUpload = () => { - cy.findByRole('button', { name: /complete upload/i }).click() - - cy.findByRole('dialog', { name: /confirm upload/i }).within(() => { - cy.findByRole('button', { name: /upload/i }).click() - }) - - cy.findByRole('heading', { - name: /uploading your/i, - level: 1 - }).should('exist') - - cy.findByRole('main').within(() => { - cy.findByRole('progressbar', { name: /upload in progress/i }).should( - 'have.attr', - 'aria-valuenow', - '0' - ) - - const assertProgress = (progress: number) => { - cy.waitUntil( - () => { - return cy - .findByRole('progressbar', { name: /upload in progress/i }) - .then((progressbar) => { - return Number(progressbar.attr('aria-valuenow')) > progress - }) - }, - { timeout: 100000, interval: 5000 } - ) - } - - assertProgress(0) - assertProgress(10) - assertProgress(20) - assertProgress(30) - assertProgress(40) - assertProgress(50) - assertProgress(60) - assertProgress(70) - assertProgress(80) - assertProgress(90) - }) - - cy.findByText(/finalizing upload/i).should('exist') - - cy.findByRole('heading', { - name: /your upload is complete/i, - level: 3, - timeout: 100000 - }).should('exist') -} - -describe('Upload Collection', () => { - beforeEach(() => { - localStorage.setItem('HAS_REQUESTED_BROWSER_PUSH_PERMISSION', 'true') - }) - - it('should upload a playlist', () => { - visitUpload() - - // Select track - - cy.findByTestId('upload-dropzone').attachFile( - ['track-small.mp3', 'track-small-2.mp3'], - { - subjectType: 'drag-n-drop' - } - ) - - cy.findByRole('radiogroup', { name: 'Release Type' }).within(() => { - cy.findByRole('radio', { name: 'Playlist' }).click({ force: true }) - }) - - cy.findByRole('button', { name: /continue uploading/i }).click() - - // Complete track form - - cy.findByRole('heading', { - name: /complete your playlist/i, - level: 1 - }).should('exist') - cy.findByRole('button', { name: /add artwork/i }).click() - - cy.findByTestId('upload-dropzone').attachFile('track-artwork.jpeg', { - subjectType: 'drag-n-drop' - }) - - cy.findByRole('textbox', { name: /Playlist Name/i }) - .clear() - .type(`Test playlist ${timestamp}`) - - let i = 1 - cy.findAllByRole('textbox', { name: /track name/i }).each(($el) => { - cy.wrap($el).clear().type(`Test track ${i++} ${timestamp}`) - }) - - cy.findByRole('combobox', { name: /pick a genre/i }) - .click() - .type('pro') - cy.findByRole('option', { name: /electronic - progressive house/i }).click() - - cy.findByRole('combobox', { name: /pick a mood/i }).click() - cy.findByRole('option', { name: /tender/i }).click() - - cy.findByRole('textbox', { name: /tags/i }) - .type('tag1{enter}') - .type('tag2') - .tab() - - cy.findByRole('textbox', { name: /playlist description/i }).type( - 'Test Description' - ) - - completeUpload() - - cy.findByRole('link', { name: /visit playlist page/i }).click() - cy.findByRole('heading', { name: /playlist/i, level: 1 }).should('exist') - - cy.findByRole('table').within(() => { - cy.findByRole('cell', { name: /Test track 1/ }) - cy.findByRole('cell', { name: /Test track 2/ }) - }) - }) - - it('should upload a album', () => { - visitUpload() - - // Select track - - cy.findByTestId('upload-dropzone').attachFile( - ['track-small.mp3', 'track-small-2.mp3'], - { - subjectType: 'drag-n-drop' - } - ) - - cy.findByRole('radiogroup', { name: 'Release Type' }).within(() => { - cy.findByRole('radio', { name: 'Album' }).click({ force: true }) - }) - - cy.findByRole('button', { name: /continue uploading/i }).click() - - // Complete track form - - cy.findByRole('heading', { - name: /complete your album/i, - level: 1 - }).should('exist') - cy.findByRole('button', { name: /add artwork/i }).click() - - cy.findByTestId('upload-dropzone').attachFile('track-artwork.jpeg', { - subjectType: 'drag-n-drop' - }) - - cy.findByRole('textbox', { name: /Album Name/i }) - .clear() - .type(`Test album ${timestamp}`) - - let i = 1 - cy.findAllByRole('textbox', { name: /track name/i }).each(($el) => { - cy.wrap($el).clear().type(`Test track ${i++} ${timestamp}`) - }) - - cy.findByRole('combobox', { name: /pick a genre/i }) - .click() - .type('pro') - cy.findByRole('option', { name: /electronic - progressive house/i }).click() - - cy.findByRole('combobox', { name: /pick a mood/i }).click() - cy.findByRole('option', { name: /tender/i }).click() - - cy.findByRole('textbox', { name: /tags/i }) - .type('tag1{enter}') - .type('tag2') - .tab() - - cy.findByRole('textbox', { name: /album description/i }).type( - 'Test Description' - ) - - completeUpload() - - cy.findByRole('link', { name: /visit album page/i }).click() - cy.findByRole('heading', { name: /album/i, level: 1 }).should('exist') - - cy.findByRole('table').within(() => { - cy.findByRole('cell', { name: /Test track 1/ }) - cy.findByRole('cell', { name: /Test track 2/ }) - }) - }) -}) diff --git a/packages/probers/cypress/e2e/uploadTrack.cy.ts b/packages/probers/cypress/e2e/uploadTrack.cy.ts deleted file mode 100644 index e50f2b91ec0..00000000000 --- a/packages/probers/cypress/e2e/uploadTrack.cy.ts +++ /dev/null @@ -1,292 +0,0 @@ -import dayjs from 'dayjs' - -import aiAttribution from '../fixtures/aiAttribution.json' -import remix from '../fixtures/remix.json' -const timestamp = dayjs().format('YYMMDD_HHmmss') - -const visitUpload = () => { - cy.login() - cy.findByRole('link', { name: /upload track/i }).click() - cy.findByRole('heading', { name: /upload your music/i, level: 1 }).should( - 'exist' - ) -} - -const completeUpload = () => { - cy.findByRole('button', { name: /complete upload/i }).click() - - cy.findByRole('dialog', { name: /confirm upload/i }).within(() => { - cy.findByRole('button', { name: /upload/i }).click() - }) - - cy.findByRole('heading', { - name: /uploading your track/i, - level: 1 - }).should('exist') - - cy.findByRole('main').within(() => { - cy.findByRole('progressbar', { name: /upload in progress/i }).should( - 'have.attr', - 'aria-valuenow', - '0' - ) - - const assertProgress = (progress: number) => { - cy.waitUntil( - () => { - return cy - .findByRole('progressbar', { name: /upload in progress/i }) - .then((progressbar) => { - return Number(progressbar.attr('aria-valuenow')) > progress - }) - }, - { timeout: 100000, interval: 5000 } - ) - } - - assertProgress(0) - assertProgress(10) - assertProgress(20) - assertProgress(30) - assertProgress(40) - assertProgress(50) - assertProgress(60) - assertProgress(70) - assertProgress(80) - assertProgress(90) - }) - - cy.findByText(/finalizing upload/i).should('exist') - - cy.findByRole('heading', { - name: /your upload is complete/i, - level: 3, - timeout: 100000 - }).should('exist') -} - -describe('Upload Track', () => { - beforeEach(() => { - localStorage.setItem('HAS_REQUESTED_BROWSER_PUSH_PERMISSION', 'true') - }) - - it('should upload a single track that is a remix and ai attributed', () => { - visitUpload() - - // Select track - - cy.findByTestId('upload-dropzone').attachFile('track-small.mp3', { - subjectType: 'drag-n-drop' - }) - cy.findByRole('button', { name: /continue uploading/i }).click() - - // Complete track form - - cy.findByRole('heading', { name: /complete your track/i, level: 1 }).should( - 'exist' - ) - cy.findByRole('button', { name: /add artwork/i }).click() - - cy.findByTestId('upload-dropzone').attachFile('track-artwork.jpeg', { - subjectType: 'drag-n-drop' - }) - - cy.findByRole('textbox', { name: /track name/i }) - .clear() - .type(`Test track ${timestamp}`) - - cy.findByRole('combobox', { name: /pick a genre/i }).click() - cy.findByRole('option', { name: /alternative/i }).click() - - cy.findByRole('combobox', { name: /pick a mood/i }).click() - cy.findByRole('option', { name: /easygoing/i }).click() - - cy.findByRole('textbox', { name: /tags/i }) - .type('tag1{enter}') - .type('tag2') - .tab() - - cy.findByRole('textbox', { name: /description/i }).type('Test Description') - - cy.findByRole('button', { name: /remix settings/i }).click() - cy.findByRole('dialog', { name: /remix settings/i }).within(() => { - cy.findByRole('checkbox', { name: /hide remixes of this track/i }).should( - 'exist' - ) - cy.findByRole('checkbox', { name: /hide remixes/i }).should('exist') - cy.findByRole('checkbox', { name: /identify as remix/i }).check() - cy.findByRole('textbox').type(`staging.audius.co/${remix.route}`) - cy.contains(`${remix.name} By df`) - cy.findByRole('button', { name: /save/i }).click() - }) - - cy.findByRole('button', { name: /access & sale/i }).click() - cy.findByRole('dialog', { name: /access & sale/i }).within(() => { - cy.findAllByRole('alert') - .first() - .within(() => { - cy.findByText(/this track is marked as a remix/i).should('exist') - }) - cy.findByRole('radiogroup', { name: /access & sale/i }).within(() => { - cy.findByRole('radio', { name: /hidden/i }).click() - cy.findByRole('group', { name: /visible track details/i }).within( - () => { - cy.findByRole('checkbox', { name: /share button/i }).click() - } - ) - }) - - cy.findByRole('button', { name: /save/i }).click() - }) - - cy.findByRole('button', { name: /attribution/i }).click() - cy.findByRole('dialog', { name: /attribution/i }).within(() => { - cy.findByRole('checkbox', { - name: /mark this track as ai generated/i - }).click() - cy.findByRole('combobox', { name: /find users/i }).type( - aiAttribution.inputName - ) - }) - cy.findByRole('option', { name: aiAttribution.name }).click() - cy.findByRole('dialog', { name: /attribution/i }).within(() => { - cy.findByRole('textbox', { name: /isrc/i }).type('US-123-45-67890') - cy.findByRole('textbox', { name: /iswc/i }).type('T-123456789-0') - cy.findByRole('radiogroup', { name: /allow attribution/i }).within(() => { - cy.findByRole('radio', { name: /allow attribution/i }).click({ - force: true // segmented controls overlap inputs - }) - }) - - cy.findByRole('radiogroup', { name: /commercial use/i }).within(() => { - cy.findByRole('radio', { name: /^commercial use/i }).click({ - force: true // segmented controls overlap inputs - }) - }) - - cy.findByRole('radiogroup', { name: /derivative works/i }).within(() => { - cy.findByRole('radio', { name: /share-alike/i }).click({ force: true }) - }) - - cy.findByRole('heading', { - name: 'Attribution ShareAlike CC BY-SA' - }).should('exist') - - cy.findByRole('button', { name: /save/i }).click() - }) - - completeUpload() - - cy.findByRole('link', { name: /visit track page/i }).click() - cy.findByRole('heading', { name: /track/i, level: 1 }).should('exist') - }) - - it('should upload a single track with a single stem', () => { - visitUpload() - - // Select track - - cy.findByTestId('upload-dropzone').attachFile('track.mp3', { - subjectType: 'drag-n-drop' - }) - cy.findByRole('button', { name: /continue uploading/i }).click() - - // Complete track form - - cy.findByRole('heading', { name: /complete your track/i, level: 1 }).should( - 'exist' - ) - cy.findByRole('button', { name: /change artwork/i }).click() - - cy.findByTestId('upload-dropzone').attachFile('track-artwork.jpeg', { - subjectType: 'drag-n-drop' - }) - - cy.findByRole('textbox', { name: /track name/i }) - .clear() - .type(`Test track stems ${timestamp}`) - - cy.findByRole('combobox', { name: /pick a genre/i }).click() - cy.findByRole('option', { name: /alternative/i }).click() - - cy.findByRole('button', { name: /stems & downloads/i }).click() - cy.findByRole('dialog', { name: /stems & downloads/i }).within(() => { - cy.findByRole('checkbox', { - name: /allow full track download/i - }).check() - cy.findByTestId('upload-dropzone').attachFile('track-small.mp3', { - subjectType: 'drag-n-drop' - }) - cy.findByRole('button', { name: /select type/i }).click() - }) - // Listbox is outside the dialog - cy.findByRole('listbox', { name: /select type/i }).within(() => { - cy.findByText(/instrumental/i).click() - }) - - // Re-enter the dialog and save - cy.findByRole('dialog', { name: /stems & downloads/i }).within(() => { - cy.findByRole('button', { name: /save/i }).click() - }) - completeUpload() - - cy.findByRole('link', { name: /visit track page/i }).click() - cy.findByRole('heading', { name: /track/i, level: 1 }).should('exist') - cy.findByRole('button', { name: /stems & downloads/i }).click() - cy.findByText(/full track/i).should('exist') - cy.findByText(/track-small.mp3/i).should('exist') - cy.findByText(/instrumental/i).should('exist') - }) - - it('should upload a single track with a pay-gate', () => { - visitUpload() - - // Select track - - cy.findByTestId('upload-dropzone').attachFile('track-small.mp3', { - subjectType: 'drag-n-drop' - }) - cy.findByRole('button', { name: /continue uploading/i }).click() - - // Complete track form - - cy.findByRole('heading', { name: /complete your track/i, level: 1 }).should( - 'exist' - ) - cy.findByRole('button', { name: /add artwork/i }).click() - - cy.findByTestId('upload-dropzone').attachFile('track-artwork.jpeg', { - subjectType: 'drag-n-drop' - }) - - cy.findByRole('textbox', { name: /track name/i }) - .clear() - .type(`Test premium pay-gated track ${timestamp}`) - - cy.findByRole('combobox', { name: /pick a genre/i }).click() - cy.findByRole('option', { name: /alternative/i }).click() - - // Set pay-gated - cy.findByRole('button', { name: /access & sale/i }).click() - cy.findByRole('dialog', { name: /access & sale/i }).within(() => { - cy.findByRole('radiogroup', { name: /access & sale/i }).within(() => { - cy.findByRole('radio', { name: /premium \(pay-to-unlock\)/i }).click() - cy.findByRole('textbox', { name: /cost to unlock/i }).type('1.05') - cy.findByRole('textbox', { name: /start time/i }).type('15') - }) - - cy.findByRole('button', { name: /save/i }).click() - }) - - completeUpload() - - cy.findByRole('link', { name: /visit track page/i }).click() - cy.findByRole('heading', { name: /pay-gated track/i, level: 1 }).should( - 'exist' - ) - cy.findByRole('button', { name: /preview/i }).should('exist') - cy.findByText(/premium track/i).should('exist') - cy.findByText(/users can unlock/i).should('exist') - cy.findByText(/purchase of \$1\.05/i).should('exist') - }) -}) diff --git a/packages/probers/cypress/fixtures/aiAttribution.json b/packages/probers/cypress/fixtures/aiAttribution.json deleted file mode 100644 index 3ed5402250d..00000000000 --- a/packages/probers/cypress/fixtures/aiAttribution.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "email": "prober+ai@audius.co", - "password": "Pa$$w0rdTest", - "route": "probersai", - "inputName": "ai DO NOT DELETE", - "name": "probers ai DO NOT DELETE" -} diff --git a/packages/probers/cypress/fixtures/album.json b/packages/probers/cypress/fixtures/album.json deleted file mode 100644 index bab244083ff..00000000000 --- a/packages/probers/cypress/fixtures/album.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "probers_album_do_not_delete", - "route": "df/album/probers_album_do_not_delete-512" -} diff --git a/packages/probers/cypress/fixtures/cover-photo.jpeg b/packages/probers/cypress/fixtures/cover-photo.jpeg deleted file mode 100644 index 974852b9538..00000000000 Binary files a/packages/probers/cypress/fixtures/cover-photo.jpeg and /dev/null differ diff --git a/packages/probers/cypress/fixtures/playlist.json b/packages/probers/cypress/fixtures/playlist.json deleted file mode 100644 index 12bc275a362..00000000000 --- a/packages/probers/cypress/fixtures/playlist.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "route": "df/playlist/probers_playlist_do_not_delete-511", - "name": "PROBERS_PLAYLIST_DO_NOT_DELETE" -} diff --git a/packages/probers/cypress/fixtures/profile-picture.jpeg b/packages/probers/cypress/fixtures/profile-picture.jpeg deleted file mode 100644 index 7defae6fa76..00000000000 Binary files a/packages/probers/cypress/fixtures/profile-picture.jpeg and /dev/null differ diff --git a/packages/probers/cypress/fixtures/ray_stack_Trace.png b/packages/probers/cypress/fixtures/ray_stack_Trace.png deleted file mode 100644 index db1b9cddf42..00000000000 Binary files a/packages/probers/cypress/fixtures/ray_stack_Trace.png and /dev/null differ diff --git a/packages/probers/cypress/fixtures/remix.json b/packages/probers/cypress/fixtures/remix.json deleted file mode 100644 index 6996604dd90..00000000000 --- a/packages/probers/cypress/fixtures/remix.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "route": "df/probers_remix_do_not_delete-2859", - "name": "probers_remix_do_not_delete" -} diff --git a/packages/probers/cypress/fixtures/remixes.json b/packages/probers/cypress/fixtures/remixes.json deleted file mode 100644 index ab92f606056..00000000000 --- a/packages/probers/cypress/fixtures/remixes.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "route": "mb430/traektoria-source-2217/remixes" -} diff --git a/packages/probers/cypress/fixtures/track-small-2.mp3 b/packages/probers/cypress/fixtures/track-small-2.mp3 deleted file mode 100644 index 4659f9fbb29..00000000000 Binary files a/packages/probers/cypress/fixtures/track-small-2.mp3 and /dev/null differ diff --git a/packages/probers/cypress/fixtures/track-small.mp3 b/packages/probers/cypress/fixtures/track-small.mp3 deleted file mode 100644 index 4659f9fbb29..00000000000 Binary files a/packages/probers/cypress/fixtures/track-small.mp3 and /dev/null differ diff --git a/packages/probers/cypress/fixtures/track.json b/packages/probers/cypress/fixtures/track.json deleted file mode 100644 index 28aab83955e..00000000000 --- a/packages/probers/cypress/fixtures/track.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "route": "sebastian12/bachgavotte-1", - "name": "probers_track_do_not_delete" -} diff --git a/packages/probers/cypress/fixtures/user.json b/packages/probers/cypress/fixtures/user.json deleted file mode 100644 index 4b890ab1e11..00000000000 --- a/packages/probers/cypress/fixtures/user.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "email": "prober+test@audius.co", - "password": "Pa$$w0rdTest", - "name": "Prober Test", - "handle": "proberTest", - "entropy": "bdaba824b6e02ab7868c5a2dfdfc7e9f" -} diff --git a/packages/probers/cypress/support/commands.ts b/packages/probers/cypress/support/commands.ts deleted file mode 100644 index 86747dee2d5..00000000000 --- a/packages/probers/cypress/support/commands.ts +++ /dev/null @@ -1,25 +0,0 @@ -import '@testing-library/cypress/add-commands' -import 'cypress-file-upload' -import 'cypress-wait-until' -import 'cypress-plugin-tab' - -import user from '../fixtures/user.json' -const base64Entropy = Buffer.from(user.entropy).toString('base64') - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Cypress { - interface Chainable { - /** - * Custom command to log in user. - * @example cy.login() - */ - login(): Chainable> - } - } -} - -Cypress.Commands.add('login', () => { - cy.visit(`trending?login=${base64Entropy}`) - cy.findByRole('link', { name: user.name }).should('exist') -}) diff --git a/packages/probers/cypress/support/e2e.ts b/packages/probers/cypress/support/e2e.ts deleted file mode 100644 index 42c4b9ce200..00000000000 --- a/packages/probers/cypress/support/e2e.ts +++ /dev/null @@ -1,7 +0,0 @@ -import './commands' - -Cypress.on('uncaught:exception', () => { - // returning false here prevents Cypress from - // failing the test - return false -}) diff --git a/packages/probers/cypress/tsconfig.json b/packages/probers/cypress/tsconfig.json deleted file mode 100644 index bf494fa7df0..00000000000 --- a/packages/probers/cypress/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": ["es5", "dom"], - "types": ["cypress", "@testing-library/cypress", "cypress-file-upload", "cypress-wait-until"], - "resolveJsonModule": true, - "esModuleInterop": true - }, - "include": ["**/*.ts"] -} \ No newline at end of file diff --git a/packages/probers/package.json b/packages/probers/package.json deleted file mode 100644 index 6544d3c7c81..00000000000 --- a/packages/probers/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "probers", - "private": true, - "version": "1.5.72", - "description": "Automated client tests", - "scripts": { - "cypress:open": "cypress open", - "cypress:open-stage": "cypress open --env configFile=stage", - "cypress:open-rc-stage": "cypress open --env configFile=rc-stage", - "cypress:run": "cypress run", - "cypress:run-stage": "cypress run --env configFile=stage", - "cypress:run-rc-stage": "cypress run --env configFile=rc-stage", - "test:target": "env-cmd -f .env jest --runInBand", - "test": "npm run cypress:run", - "test:ci": "npm run cypress:run", - "lint": "eslint --cache --ext=js,jsx,ts,tsx cypress", - "lint:fix": "eslint --cache --fix --ext=js,jsx,ts,tsx cypress" - }, - "author": "", - "license": "ISC", - "dependencies": { - "@testing-library/cypress": "^9.0.0", - "cypress": "^13.0.0", - "cypress-file-upload": "^5.0.8", - "cypress-wait-until": "^1.7.2", - "dayjs": "^1.11.6", - "lodash": "4.17.21", - "web3": "4.1.1" - }, - "standard": { - "env": [ - "jest" - ], - "ignore": [] - }, - "devDependencies": { - "cypress-plugin-tab": "^1.0.5", - "eslint": "8.56.0", - "eslint-plugin-cypress": "2.12.1", - "typescript": "5.0.4" - } -} diff --git a/packages/spl/package.json b/packages/spl/package.json index 1a6170b8d29..505aea4d3ca 100644 --- a/packages/spl/package.json +++ b/packages/spl/package.json @@ -1,6 +1,6 @@ { "name": "@audius/spl", - "version": "0.0.28", + "version": "0.0.29", "description": "The core Typescript mappings to the Audius Solana Programs", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/packages/sql-ts/package.json b/packages/sql-ts/package.json index 33fdb554e89..9297b116ff4 100644 --- a/packages/sql-ts/package.json +++ b/packages/sql-ts/package.json @@ -1,7 +1,7 @@ { "name": "@audius/sql-ts", "private": true, - "version": "1.0.30", + "version": "1.0.31", "description": "", "scripts": { "start": "ts-node src/index.ts" diff --git a/packages/trpc-server/package.json b/packages/trpc-server/package.json index f4d0747ea50..67fd9dbd19d 100644 --- a/packages/trpc-server/package.json +++ b/packages/trpc-server/package.json @@ -1,6 +1,6 @@ { "name": "@audius/trpc-server", - "version": "0.0.28", + "version": "0.0.29", "private": true, "main": "src/index.ts", "scripts": { diff --git a/packages/web/.gitignore b/packages/web/.gitignore index f641c6ea719..797bf6b97db 100644 --- a/packages/web/.gitignore +++ b/packages/web/.gitignore @@ -60,3 +60,7 @@ yarn-error.log* .idea .yalc yalc.lock +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/ diff --git a/packages/web/e2e/auth.setup.ts b/packages/web/e2e/auth.setup.ts new file mode 100644 index 00000000000..0fc66178482 --- /dev/null +++ b/packages/web/e2e/auth.setup.ts @@ -0,0 +1,15 @@ +import { expect } from '@playwright/test' +import { test as setup } from './test' + +const base64Entropy = 'YmRhYmE4MjRiNmUwMmFiNzg2OGM1YTJkZmRmYzdlOWY' +const authFile = 'playwright/.auth/user.json' + +setup('authenticate', async ({ page }) => { + await page.goto(`/feed?login=${base64Entropy}`) + const usernameLocator = page.getByText('probertest') + await expect(usernameLocator).toBeVisible() + await page.evaluate(() => { + localStorage.setItem('HAS_REQUESTED_BROWSER_PUSH_PERMISSION', 'true') + }) + await page.context().storageState({ path: authFile }) +}) diff --git a/packages/probers/cypress/fixtures/track.mp3 b/packages/web/e2e/files/stem-1.mp3 similarity index 100% rename from packages/probers/cypress/fixtures/track.mp3 rename to packages/web/e2e/files/stem-1.mp3 diff --git a/packages/web/e2e/files/stem-2.mp3 b/packages/web/e2e/files/stem-2.mp3 new file mode 100644 index 00000000000..29073a5fe47 Binary files /dev/null and b/packages/web/e2e/files/stem-2.mp3 differ diff --git a/packages/web/e2e/files/track-2.mp3 b/packages/web/e2e/files/track-2.mp3 new file mode 100644 index 00000000000..29073a5fe47 Binary files /dev/null and b/packages/web/e2e/files/track-2.mp3 differ diff --git a/packages/probers/cypress/fixtures/track-artwork.jpeg b/packages/web/e2e/files/track-artwork.jpeg similarity index 100% rename from packages/probers/cypress/fixtures/track-artwork.jpeg rename to packages/web/e2e/files/track-artwork.jpeg diff --git a/packages/web/e2e/files/track.mp3 b/packages/web/e2e/files/track.mp3 new file mode 100644 index 00000000000..29073a5fe47 Binary files /dev/null and b/packages/web/e2e/files/track.mp3 differ diff --git a/packages/web/e2e/page-object-models/modals.ts b/packages/web/e2e/page-object-models/modals.ts new file mode 100644 index 00000000000..6d234170f96 --- /dev/null +++ b/packages/web/e2e/page-object-models/modals.ts @@ -0,0 +1,235 @@ +import { Locator, Page } from '@playwright/test' +import path from 'path' + +export class StemsAndDownloadsModal { + public readonly locator: Locator + + private readonly trackDownloadInput: Locator + private readonly dropzoneFileInput: Locator + private readonly saveButton: Locator + + constructor(page: Page) { + this.locator = page.getByRole('dialog', { + name: /stems & downloads/i + }) + this.trackDownloadInput = this.locator.getByRole('checkbox', { + name: /allow full track download/i + }) + this.dropzoneFileInput = this.locator + .getByTestId('upload-dropzone') + .locator('input[type=file]') + this.saveButton = this.locator.getByRole('button', { name: /save/i }) + } + + async setAllowTrackDownload(allow: boolean) { + if (allow) { + await this.trackDownloadInput.check() + } else { + await this.trackDownloadInput.uncheck() + } + } + + async setStems(files: Array<{ filename: string; type?: string } | string>) { + await this.dropzoneFileInput.setInputFiles( + files.map((file) => + path.join( + __dirname, + '..', + 'files', + typeof file === 'string' ? file : file.filename + ) + ) + ) + for (const file of files) { + if (typeof file === 'string' || !file.type) { + continue + } + await this.locator + .getByRole('listitem') + .filter({ hasText: file.filename }) + .getByRole('button', { name: /select type/i }) + .click() + await this.locator + .page() + .getByRole('listbox', { name: /select type/i }) + .getByRole('option', { name: file.type }) + .click() + } + } + + async save() { + await this.saveButton.click() + } +} + +export class AttributionModal { + private readonly locator: Locator + private readonly allowAttribution: Locator + private readonly commercialUse: Locator + private readonly derivativeWorks: Locator + private readonly saveButton: Locator + + constructor(page: Page) { + this.locator = page.getByRole('dialog', { name: /attribution/i }) + this.allowAttribution = this.locator.getByRole('radiogroup', { + name: /allow attribution/i + }) + this.commercialUse = this.locator.getByRole('radiogroup', { + name: /commercial use/i + }) + this.derivativeWorks = this.locator.getByRole('radiogroup', { + name: /derivative works/i + }) + this.saveButton = this.locator.getByRole('button', { name: /save/i }) + } + + async markAsAIGenerated(user: string) { + await this.locator + .getByRole('checkbox', { + name: /mark this track as ai generated/i + }) + .click() + await this.locator.getByRole('combobox', { name: /find users/i }).fill(user) + // This option is mounted to the page + await this.locator.page().getByRole('option', { name: user }).click() + } + + async setISRC(isrc: string) { + await this.locator.getByRole('textbox', { name: /isrc/i }).fill(isrc) + } + async setISWC(iswc: string) { + await this.locator.getByRole('textbox', { name: /iswc/i }).fill(iswc) + } + + async setAllowAttribution(allow: boolean) { + if (allow) { + await this.allowAttribution + .getByRole('radio', { name: /allow attribution/i }) + .check({ force: true }) // segmented control + } else { + await this.allowAttribution + .getByRole('radio', { name: /no attribution/i }) + .check({ force: true }) // segmented control + } + } + + async setAllowCommercialUse(allow: boolean) { + if (allow) { + await this.commercialUse + .getByRole('radio', { name: /^commercial use/i }) + .check({ force: true }) // segmented control + } else { + await this.commercialUse + .getByRole('radio', { name: /non-commercial use/i }) + .check({ force: true }) // segmented control + } + } + + async setDerivativeWorks( + permission: 'Not-Allowed' | 'Share-Alike' | 'Allowed' + ) { + await this.derivativeWorks + .getByRole('radio', { name: permission, exact: true }) + .check({ force: true }) // segmented control + } + + async save() { + await this.saveButton.click() + } +} + +type VisibleDetail = 'Genre' | 'Mood' | 'Tags' | 'Share Button' | 'Play Count' +export class AccessAndSaleModal { + public readonly locator: Locator + public readonly remixAlert: Locator + + private readonly radioGroup: Locator + private readonly visibleTrackDetails: Locator + private readonly priceInput: Locator + private readonly previewSecondsInput: Locator + private readonly saveButton: Locator + + constructor(page: Page) { + this.locator = page.getByRole('dialog', { + name: /access & sale/i + }) + this.remixAlert = this.locator + .getByRole('alert') + .first() + .getByText('this track is marked as a remix') + this.radioGroup = this.locator.getByRole('radiogroup', { + name: /access & sale/i + }) + this.visibleTrackDetails = this.radioGroup.getByRole('group', { + name: /visible track details/i + }) + this.priceInput = this.radioGroup.getByRole('textbox', { + name: /cost to unlock/i + }) + this.previewSecondsInput = this.radioGroup.getByRole('textbox', { + name: /start time/i + }) + this.saveButton = this.locator.getByRole('button', { name: /save/i }) + } + + async save() { + await this.saveButton.click() + } + + async setHidden(visibleDetails: Partial>) { + await this.radioGroup.getByRole('radio', { name: /hidden/i }).check() + for (const name of Object.keys(visibleDetails)) { + const checkbox = this.visibleTrackDetails.getByRole('checkbox', { name }) + if (visibleDetails[name]) { + await checkbox.check() + } else { + await checkbox.uncheck() + } + } + } + + async setPremium({ + price, + previewSeconds + }: { + price: string + previewSeconds: string + }) { + await this.radioGroup + .getByRole('radio', { + name: /premium \(pay-to-unlock\)/i + }) + .check() + await this.priceInput.fill(price) + await this.previewSecondsInput.fill(previewSeconds) + } +} + +export class RemixSettingsModal { + public readonly locator: Locator + + constructor(page: Page) { + this.locator = page.getByRole('dialog', { + name: /remix settings/i + }) + } + + async hideRemixes() { + await this.locator + .getByRole('checkbox', { name: /hide remixes of this track/i }) + .check() + } + + async setAsRemixOf(remixUrl: string, remixTitle: string) { + await this.locator + .getByRole('checkbox', { name: /identify as remix/i }) + .check() + await this.locator.getByRole('textbox').pressSequentially(remixUrl) + const remixTrack = this.locator.getByText(remixTitle).first() + await remixTrack.click() + } + + async save() { + await this.locator.getByRole('button', { name: /save/i }).click() + } +} diff --git a/packages/web/e2e/page-object-models/socialActions.ts b/packages/web/e2e/page-object-models/socialActions.ts new file mode 100644 index 00000000000..1adb22d6180 --- /dev/null +++ b/packages/web/e2e/page-object-models/socialActions.ts @@ -0,0 +1,102 @@ +import { Locator, Page, expect } from '@playwright/test' + +export class SocialActions { + private readonly page: Page + public readonly favoriteButton: Locator + public readonly unfavoriteButton: Locator + public readonly repostButton: Locator + public readonly unrepostButton: Locator + + constructor(page: Page) { + this.page = page + const trackActions = page.getByRole('group', { + name: /track actions/i + }) + this.favoriteButton = trackActions.getByRole('button', { + name: /favorite$/i + }) + this.unfavoriteButton = trackActions.getByRole('button', { + name: /favorited$/i + }) + this.repostButton = trackActions.getByRole('button', { + name: /repost$/i + }) + this.unrepostButton = trackActions.getByRole('button', { + name: /reposted$/i + }) + } + + async waitForConfirmation() { + return this.page + .waitForResponse(async (response) => { + if (response.url().includes('block_confirmation')) { + const json = await response.json() + return json.data.block_passed + } + }) + .catch((e) => { + // Swallow error - prevents flakes + console.warn('Confirmation timed out', e) + }) + } + + async favorite() { + // Setup confirmation listener + const confirmationPromise = this.waitForConfirmation() + + // Unfavorited => Favorited + await this.favoriteButton.click() + + // Wait for indexing, reload + await confirmationPromise + await this.favoriteButton.page().reload() + + // Check that it persists after reload + await expect(this.unfavoriteButton).toBeVisible() + } + + async unfavorite() { + // Setup confirmation listener + const confirmationPromise = this.waitForConfirmation() + + // Favorited => Unfavorited + await this.unfavoriteButton.click() + + // Wait for indexing, reload + await confirmationPromise + await this.unfavoriteButton.page().reload() + + // Check that it persists after reload + await expect(this.favoriteButton).toBeVisible() + } + + async repost() { + // Setup confirmation listener + const confirmationPromise = this.waitForConfirmation() + + // Unreposted => Reposted + await this.repostButton.click() + + // Wait for indexing, reload + await confirmationPromise + await this.repostButton.page().reload() + + // Check that it persists after reload + await expect(this.unrepostButton).toBeVisible() + } + + async unrepost() { + // Setup confirmation listener + const confirmationPromise = this.waitForConfirmation() + + // Reposted => Unreposted + await this.unrepostButton.click() + + // Wait for indexing, reload + await confirmationPromise + await this.unrepostButton.page().reload() + + // Check that it persists after reload + await expect(this.repostButton).toBeVisible() + } +} diff --git a/packages/web/e2e/page-object-models/upload.ts b/packages/web/e2e/page-object-models/upload.ts new file mode 100644 index 00000000000..d9f340c0e68 --- /dev/null +++ b/packages/web/e2e/page-object-models/upload.ts @@ -0,0 +1,293 @@ +import { Locator, Page, expect } from '@playwright/test' +import path from 'path' + +class BaseEditPage { + protected readonly artworkButton: Locator + protected readonly dropzoneFileInput: Locator + protected readonly titleInput: Locator + protected readonly genreBox: Locator + protected readonly genreList: Locator + protected readonly moodBox: Locator + protected readonly moodList: Locator + protected readonly tagsInput: Locator + protected readonly descriptionInput: Locator + protected readonly completeButton: Locator + + constructor(page: Page) { + this.artworkButton = page.getByRole('button', { name: /artwork$/i }) + this.dropzoneFileInput = page + .getByTestId('upload-dropzone') + .locator('input[type=file]') + this.genreBox = page.getByRole('combobox', { name: /pick a genre/i }) + this.genreList = page + .getByLabel(/pick a genre/i) + .locator('.rc-virtual-list') // ant-d uses a virtualized list + this.moodBox = page.getByRole('combobox', { name: /pick a mood/i }) + this.moodList = page.getByLabel(/pick a mood/i).locator('.rc-virtual-list') // ant-d uses a virtualized list + this.tagsInput = page.getByRole('textbox', { name: /tags/i }) + this.descriptionInput = page.getByRole('textbox', { name: /description/i }) + this.completeButton = page.getByRole('button', { name: /complete upload/i }) + } + + async setArtwork(file: string) { + await this.artworkButton.click() + await this.dropzoneFileInput.setInputFiles( + path.join(__dirname, '..', 'files', file) + ) + await this.dropzoneFileInput.page().getByLabel('close popup').click() + } + + async setTitle(title: string) { + await this.titleInput.fill(title) + } + + async setGenre(genre: string) { + await this.genreBox.fill(genre) + await this.genreList.getByRole('option', { name: genre }).click() + } + + async setMood(mood: string) { + await this.moodBox.fill(mood) + await this.moodList.getByRole('option', { name: mood }).click() + } + + async setTags(tags: string[]) { + for (const tag of tags) { + await this.tagsInput.fill(tag) + await this.tagsInput.press('Enter') + } + } + + async setDescription(description: string) { + await this.descriptionInput.fill(description) + } + + async complete() { + await this.completeButton.click() + const confirmUploadModal = this.completeButton.page().getByRole('dialog', { + name: /confirm upload/i + }) + await confirmUploadModal.getByRole('button', { name: /upload/i }).click() + } +} + +export class EditTrackPage extends BaseEditPage { + protected readonly titleInput: Locator + private readonly remixSettingsButton: Locator + private readonly accessAndSaleSettingsButton: Locator + private readonly attributionSettingsButton: Locator + private readonly stemsAndDownloadsSettingsButton: Locator + + constructor(page: Page) { + super(page) + this.titleInput = page.getByRole('textbox', { name: /track name/i }) + this.remixSettingsButton = page.getByRole('button', { + name: /remix settings/i + }) + this.accessAndSaleSettingsButton = page.getByRole('button', { + name: /access & sale/i + }) + this.attributionSettingsButton = page.getByRole('button', { + name: /attribution/i + }) + this.stemsAndDownloadsSettingsButton = page.getByRole('button', { + name: /stems & downloads/i + }) + } + + async openRemixSettings() { + await this.remixSettingsButton.click() + } + + async openAccessAndSaleSettings() { + await this.accessAndSaleSettingsButton.click() + } + + async openAttributionSettings() { + await this.attributionSettingsButton.click() + } + + async openStemsAndDownloadsSettings() { + await this.stemsAndDownloadsSettingsButton.click() + } +} + +export class EditPlaylistPage extends BaseEditPage { + protected readonly titleInput: Locator + protected readonly trackList: Locator + + constructor(page: Page) { + super(page) + this.titleInput = page.getByRole('textbox', { name: /playlist name/i }) + this.trackList = page.getByRole('list', { name: /track list/i }) + } + + async setTrackName(index: number, name: string) { + const trackItem = this.trackList.getByRole('listitem').nth(index) + const nameInput = trackItem.getByRole('textbox', { name: /track name/i }) + await nameInput.fill(name) + } + + async setTrackDetails( + index: number, + { + name, + genre, + mood, + tags + }: { + name?: string + genre?: string + mood?: string + tags?: string[] + } + ) { + const trackItem = this.trackList.getByRole('listitem').nth(index) + const checkbox = trackItem.getByRole('checkbox', { + name: /override details for this track/i + }) + if (genre || mood || tags) { + await checkbox.check() + } + if (name) { + await this.setTrackName(index, name) + } + if (genre) { + await this.setTrackGenre(index, genre) + } + if (mood) { + await this.setTrackMood(index, mood) + } + if (tags) { + await this.addTrackTags(index, tags) + } + } + + private async setTrackGenre(index: number, genre: string) { + const trackItem = this.trackList.getByRole('listitem').nth(index) + const genreBox = trackItem.getByRole('combobox', { name: /pick a genre/i }) + const genreList = genreBox + .page() + .getByLabel(/pick a genre/i) + .locator('.rc-virtual-list') // ant-d uses a virtualized list + await genreBox.fill(genre) + await genreList.getByRole('option', { name: genre }).click() + } + + private async setTrackMood(index: number, mood: string) { + const trackItem = this.trackList.getByRole('listitem').nth(index) + const moodBox = trackItem.getByRole('combobox', { name: /pick a mood/i }) + const moodList = moodBox + .page() + .getByLabel(/pick a mood/i) + .locator('.rc-virtual-list') // ant-d uses a virtualized list + await moodBox.fill(mood) + await moodList.getByRole('option', { name: mood }).click() + } + + private async addTrackTags(index: number, tags: string[]) { + const trackItem = this.trackList.getByRole('listitem').nth(index) + const tagsInput = trackItem.getByRole('textbox', { name: /tags/i }) + for (const tag of tags) { + await tagsInput.fill(tag) + await tagsInput.press('Enter') + } + } +} + +export class EditAlbumPage extends EditPlaylistPage { + protected readonly titleInput: Locator + + constructor(page: Page) { + super(page) + this.titleInput = page.getByRole('textbox', { name: /album name/i }) + } +} + +export class UploadSelectPage { + private readonly dropzoneFileInput: Locator + private readonly releaseType: Locator + private readonly continueButton: Locator + + constructor(page: Page) { + this.dropzoneFileInput = page + .getByTestId('upload-dropzone') + .locator('input[type=file]') + this.releaseType = page.getByRole('radiogroup', { + name: /release type/i + }) + this.continueButton = page.getByRole('button', { + name: /continue uploading/i + }) + } + + async setTracks(...files: string[]) { + await this.dropzoneFileInput.setInputFiles( + files.map((file) => path.join(__dirname, '..', 'files', file)) + ) + } + + async setReleaseType(type: 'Track' | 'Playlist' | 'Album') { + await this.releaseType + .getByRole('radio', { name: type }) + .check({ force: true }) + } + + async continue() { + await this.continueButton.click() + } +} + +export class UploadFinishPage { + private readonly progressBar: Locator + private readonly uploadingHeading: Locator + private readonly finalizing: Locator + private readonly successHeading: Locator + + constructor(page: Page, type: 'Track' | 'Tracks' | 'Album' | 'Playlist') { + this.progressBar = page.getByRole('progressbar', { + name: /upload in progress/i + }) + this.uploadingHeading = page.getByRole('heading', { + name: `Uploading Your ${type}` + }) + this.finalizing = page.getByText(/finalizing upload/i) + this.successHeading = page.getByRole('heading', { + name: /your upload is complete/i + }) + } + + async assertCompletes() { + // Assert uploading + await expect(this.uploadingHeading).toBeVisible() + + // Assert progress + await this.assertProgress(1) + await this.assertProgress(10) + await this.assertProgress(20) + await this.assertProgress(30) + await this.assertProgress(40) + await this.assertProgress(50) + await this.assertProgress(60) + await this.assertProgress(70) + await this.assertProgress(80) + await this.assertProgress(90) + await this.assertProgress(99) + + // Assert finalizing + await expect(this.finalizing).toBeVisible({ timeout: 60 * 1000 }) + + // Assert success + await expect(this.successHeading).toBeVisible({ timeout: 60 * 1000 }) + } + + private async assertProgress(progress: number) { + await expect + .poll( + async () => + Number(await this.progressBar.getAttribute('aria-valuenow')), + { timeout: 60 * 1000 } + ) + .toBeGreaterThanOrEqual(progress) + } +} diff --git a/packages/web/e2e/play.test.ts b/packages/web/e2e/play.test.ts new file mode 100644 index 00000000000..90a5d8c6a9e --- /dev/null +++ b/packages/web/e2e/play.test.ts @@ -0,0 +1,36 @@ +import { expect } from '@playwright/test' +import { test } from './test' + +declare global { + interface Window { + audio: { + paused: boolean + } + } +} + +test('should play a trending track', async ({ page }) => { + await page.goto('trending') + + const trendingList = page.getByRole('list', { + name: /weekly trending tracks/i + }) + const skeletons = trendingList.locator('[aria-busy]') + const items = trendingList.getByRole('listitem') + const playButton = page.getByRole('button', { name: /play track/i }) + const pauseButton = page.getByRole('button', { name: /pause track/i }) + const playHoverIcon = items.first().getByRole('img', { name: 'Play' }) + + const isPlayingWatcher = page.waitForFunction(() => !window.audio.paused) + const isPausedWatcher = page.waitForFunction(() => window.audio.paused) + + await expect(skeletons).toHaveCount(0) + await playHoverIcon.hover() + await expect(playHoverIcon).toBeVisible() + await playHoverIcon.click() + await expect(pauseButton).toBeVisible() + await isPlayingWatcher + await pauseButton.click() + await expect(playButton).toBeVisible() + await isPausedWatcher +}) diff --git a/packages/web/e2e/smoke.test.ts b/packages/web/e2e/smoke.test.ts new file mode 100644 index 00000000000..621febdf302 --- /dev/null +++ b/packages/web/e2e/smoke.test.ts @@ -0,0 +1,66 @@ +import { expect } from '@playwright/test' +import { test } from './test' + +test('should load an album page', async ({ page }) => { + await page.goto('df/album/probers_album_do_not_delete-512') + const heading = page.getByRole('heading', { + name: 'probers_album_do_not_delete', + level: 1 + }) + await expect(heading).toBeVisible() +}) + +test('should load the feed page', async ({ page }) => { + await page.goto('feed') + const heading = page.getByRole('heading', { name: 'Your Feed', level: 1 }) + await expect(heading).toBeVisible() +}) + +test('should load a playlist page', async ({ page }) => { + await page.goto('df/playlist/probers_playlist_do_not_delete-511') + const heading = page.getByRole('heading', { + name: 'PROBERS_PLAYLIST_DO_NOT_DELETE' + }) + await expect(heading).toBeVisible() +}) + +test('should load a remix page', async ({ page }) => { + await page.goto('df/probers_remix_do_not_delete-2859') + const heading = page.getByRole('heading', { + name: 'probers_remix_do_not_delete' + }) + await expect(heading).toBeVisible() +}) + +test('should load a remixes page', async ({ page }) => { + await page.goto('mb430/traektoria-source-2217/remixes') + const heading = page.getByRole('heading', { name: 'Remixes', level: 1 }) + await expect(heading).toBeVisible() +}) + +test('should load a track page', async ({ page }) => { + await page.goto('sebastian12/bachgavotte-1') + const heading = page.getByRole('heading', { + name: 'probers_track_do_not_delete', + level: 1 + }) + await expect(heading).toBeVisible() +}) + +test('should load trending page', async ({ page }) => { + await page.goto('trending') + const heading = page.getByRole('heading', { + name: 'Trending', + level: 1 + }) + await expect(heading).toBeVisible() +}) + +test('should load upload page', async ({ page }) => { + await page.goto(`upload`) + const heading = page.getByRole('heading', { + name: 'Upload Your Music', + level: 1 + }) + await expect(heading).toBeVisible() +}) diff --git a/packages/web/e2e/socials.test.ts b/packages/web/e2e/socials.test.ts new file mode 100644 index 00000000000..03c39b6ab45 --- /dev/null +++ b/packages/web/e2e/socials.test.ts @@ -0,0 +1,44 @@ +import { expect } from '@playwright/test' +import { SocialActions } from './page-object-models/socialActions' +import { test } from './test' + +test('should favorite/unfavorite a track', async ({ page }) => { + await page.goto('sebastian12/bachgavotte-1') + + const socialActions = new SocialActions(page) + // On initial load, the favorite button will be showing. Make sure to wait for + // the unfavorite button in case it comes, but allow failure. + await expect( + socialActions.unfavoriteButton, + 'Check for unfavorite (ok if fails)' + ) + .toBeVisible() + .catch(() => {}) + + if (await socialActions.unfavoriteButton.isVisible()) { + await socialActions.unfavorite() + await socialActions.favorite() + } else { + await socialActions.favorite() + await socialActions.unfavorite() + } +}) + +test('should repost/unrepost a track', async ({ page }) => { + await page.goto('sebastian12/bachgavotte-1') + + const socialActions = new SocialActions(page) + // On initial load, the repost button will be showing. Make sure to wait for + // the unrepost button in case it comes, but continue if it doesn't. + await expect(socialActions.unrepostButton, 'Check for unrepost (ok if fails)') + .toBeVisible() + .catch(() => {}) + + if (await socialActions.unrepostButton.isVisible()) { + await socialActions.unrepost() + await socialActions.repost() + } else { + await socialActions.repost() + await socialActions.unrepost() + } +}) diff --git a/packages/web/e2e/test.ts b/packages/web/e2e/test.ts new file mode 100644 index 00000000000..ecd8a1498a0 --- /dev/null +++ b/packages/web/e2e/test.ts @@ -0,0 +1,34 @@ +import { Page, expect, test as base } from '@playwright/test' + +/** + * The initial page load is slow because we need to wait for the + * JS to hydrate, which takes a while. These wrappers wait until a specific + * client-only element is mounted before considering the navigation complete. + */ +export const test = base.extend<{}>({ + page: async ({ page }, use) => { + const baseGoTo = page.goto.bind(page) + page.goto = async ( + url: Parameters[0], + options: Parameters[1] = {} + ) => { + const timeout = options.timeout ?? 60 * 1000 + const response = await baseGoTo(url, { waitUntil: 'load', ...options }) + await expect(page.getByTestId('app-hydrated')).toBeAttached({ + timeout + }) + return response + } + + const baseReload = page.reload.bind(page) + page.reload = async (options: Parameters[0]) => { + const timeout = options?.timeout ?? 60 * 1000 + const reponse = await baseReload(options) + await expect(page.getByTestId('app-hydrated')).toBeAttached({ + timeout + }) + return reponse + } + await use(page) + } +}) diff --git a/packages/web/e2e/uploadCollection.test.ts b/packages/web/e2e/uploadCollection.test.ts new file mode 100644 index 00000000000..d6028452f07 --- /dev/null +++ b/packages/web/e2e/uploadCollection.test.ts @@ -0,0 +1,190 @@ +import { expect } from '@playwright/test' +import { + EditAlbumPage, + EditPlaylistPage, + UploadFinishPage, + UploadSelectPage +} from './page-object-models/upload' +import { test } from './test' + +test('should upload a playlist', async ({ page }) => { + const timestamp = Date.now() + const playlistName = `Test playlist ${timestamp}` + const playlistDescription = 'Test description' + const trackOneDetails = { name: `Test track 1 ${timestamp}` } + const trackTwoDetails = { + name: `Test track 2 ${timestamp}`, + genre: 'Alternative', + mood: 'Energizing', + tags: ['TAG3', 'TAG4'] + } + const trackDetails = [trackOneDetails, trackTwoDetails] + const genre = 'Electronic - Progressive House' + const mood = 'Tender' + const tags = ['TAG1', 'TAG2'] + + await page.goto('upload') + + const selectPage = new UploadSelectPage(page) + await selectPage.setTracks('track.mp3', 'track-2.mp3') + await selectPage.setReleaseType('Playlist') + await selectPage.continue() + + await expect( + page.getByRole('heading', { name: /complete your playlist/i }) + ).toBeVisible() + + const editPage = new EditPlaylistPage(page) + await editPage.setArtwork('track-artwork.jpeg') + await editPage.setTitle(playlistName) + await editPage.setGenre(genre) + await editPage.setMood(mood) + await editPage.setTags(tags) + await editPage.setDescription(playlistDescription) + + for (let i = 0; i < trackDetails.length; i++) { + await editPage.setTrackDetails(i, trackDetails[i]) + } + + await editPage.complete() + + const finishPage = new UploadFinishPage(page, 'Playlist') + await finishPage.assertCompletes() + + // Vist collection page + await page.getByRole('link', { name: /visit playlist page/i }).click() + + // Assert title + const header = page.getByRole('heading', { name: playlistName, level: 1 }) + await expect(header).toBeVisible() + + // Assert description + const description = page.getByText(playlistDescription) + await expect(description).toBeVisible() + + // Assert track list + const trackTable = page.getByRole('table') + const trackOne = trackTable.getByRole('cell', { name: trackOneDetails.name }) + const trackTwo = trackTable.getByRole('cell', { name: trackTwoDetails.name }) + await expect(trackOne).toBeVisible() + await expect(trackTwo).toBeVisible() + + // Visit track 1 + await trackOne.getByRole('link', { name: trackOneDetails.name }).click() + + // Assert tagged + const tag1 = page.getByRole('link', { name: tags[0] }) + const tag2 = page.getByRole('link', { name: tags[1] }) + await expect(tag1).toBeVisible() + await expect(tag2).toBeVisible() + + // Assert genre and mood + await expect(page.getByText(genre)).toBeVisible() + await expect(page.getByText(mood)).toBeVisible() + + await page.goBack() + + // Visit track 2 + await trackTwo.getByRole('link', { name: trackTwoDetails.name }).click() + + // Assert tagged differently + const tag3 = page.getByRole('link', { name: trackTwoDetails.tags[0] }) + const tag4 = page.getByRole('link', { name: trackTwoDetails.tags[1] }) + await expect(tag3).toBeVisible() + await expect(tag4).toBeVisible() + + // Assert different genre and mood + await expect(page.getByText(trackTwoDetails.genre)).toBeVisible() + await expect(page.getByText(trackTwoDetails.mood)).toBeVisible() +}) + +test('should upload an album', async ({ page }) => { + const timestamp = Date.now() + const playlistName = `Test album ${timestamp}` + const playlistDescription = 'Test description' + const trackOneDetails = { name: `Test track 1 ${timestamp}` } + const trackTwoDetails = { + name: `Test track 2 ${timestamp}`, + genre: 'Alternative', + mood: 'Energizing', + tags: ['TAG3', 'TAG4'] + } + const trackDetails = [trackOneDetails, trackTwoDetails] + const genre = 'Electronic - Progressive House' + const mood = 'Tender' + const tags = ['TAG1', 'TAG2'] + + await page.goto('upload') + + const selectPage = new UploadSelectPage(page) + await selectPage.setTracks('track.mp3', 'track-2.mp3') + await selectPage.setReleaseType('Album') + await selectPage.continue() + + await expect( + page.getByRole('heading', { name: /complete your album/i }) + ).toBeVisible() + + const editPage = new EditAlbumPage(page) + await editPage.setArtwork('track-artwork.jpeg') + await editPage.setTitle(playlistName) + await editPage.setGenre(genre) + await editPage.setMood(mood) + await editPage.setTags(tags) + await editPage.setDescription(playlistDescription) + + for (let i = 0; i < trackDetails.length; i++) { + await editPage.setTrackDetails(i, trackDetails[i]) + } + + await editPage.complete() + + const finishPage = new UploadFinishPage(page, 'Album') + await finishPage.assertCompletes() + + // Vist collection page + await page.getByRole('link', { name: /visit album page/i }).click() + + // Assert title + const header = page.getByRole('heading', { name: playlistName, level: 1 }) + await expect(header).toBeVisible() + + // Assert description + const description = page.getByText(playlistDescription) + await expect(description).toBeVisible() + + // Assert track list + const trackTable = page.getByRole('table') + const trackOne = trackTable.getByRole('cell', { name: trackOneDetails.name }) + const trackTwo = trackTable.getByRole('cell', { name: trackTwoDetails.name }) + await expect(trackOne).toBeVisible() + await expect(trackTwo).toBeVisible() + + // Visit track 1 + await trackOne.getByRole('link', { name: trackOneDetails.name }).click() + + // Assert tagged + const tag1 = page.getByRole('link', { name: tags[0] }) + const tag2 = page.getByRole('link', { name: tags[1] }) + await expect(tag1).toBeVisible() + await expect(tag2).toBeVisible() + + // Assert genre and mood + await expect(page.getByText(genre)).toBeVisible() + await expect(page.getByText(mood)).toBeVisible() + + await page.goBack() + + // Visit track 2 + await trackTwo.getByRole('link', { name: trackTwoDetails.name }).click() + + // Assert tagged differently + const tag3 = page.getByRole('link', { name: trackTwoDetails.tags[0] }) + const tag4 = page.getByRole('link', { name: trackTwoDetails.tags[1] }) + await expect(tag3).toBeVisible() + await expect(tag4).toBeVisible() + + // Assert different genre and mood + await expect(page.getByText(trackTwoDetails.genre)).toBeVisible() + await expect(page.getByText(trackTwoDetails.mood)).toBeVisible() +}) diff --git a/packages/web/e2e/uploadTrack.test.ts b/packages/web/e2e/uploadTrack.test.ts new file mode 100644 index 00000000000..a5e9ff6de20 --- /dev/null +++ b/packages/web/e2e/uploadTrack.test.ts @@ -0,0 +1,224 @@ +import { expect } from '@playwright/test' +import { + EditTrackPage, + UploadFinishPage, + UploadSelectPage +} from './page-object-models/upload' +import { + RemixSettingsModal, + AccessAndSaleModal, + AttributionModal, + StemsAndDownloadsModal +} from './page-object-models/modals' +import { test } from './test' + +test('should upload a remix, hidden, AI-attributed track', async ({ page }) => { + const trackTitle = `Test track ${Date.now()}` + const trackDescription = 'Test description' + const genre = 'Alternative' + const mood = 'Easygoing' + const tags = ['TAG1', 'TAG2'] + const remixUrl = 'staging.audius.co/sebastian12/probers_track_do_not_delete' + const remixName = 'probers_track_do_not_delete' + const aiAttributionName = 'probers ai DO NOT DELETE' + const isrc = 'US-123-45-67890' + const iswc = 'T-123456789-0' + + await page.goto('upload') + + const selectPage = new UploadSelectPage(page) + await selectPage.setTracks('track.mp3') + await selectPage.continue() + + await expect( + page.getByRole('heading', { name: /complete your track/i, level: 1 }) + ).toBeVisible() + + const editPage = new EditTrackPage(page) + await editPage.setArtwork('track-artwork.jpeg') + await editPage.setTitle(trackTitle) + await editPage.setDescription(trackDescription) + await editPage.setGenre(genre) + await editPage.setMood(mood) + await editPage.setTags(tags) + + await editPage.openRemixSettings() + const remixSettingsModal = new RemixSettingsModal(page) + await remixSettingsModal.setAsRemixOf(remixUrl, remixName) + await remixSettingsModal.save() + + await editPage.openAccessAndSaleSettings() + const accessAndSaleModal = new AccessAndSaleModal(page) + expect(accessAndSaleModal.remixAlert).toBeVisible() + // Only public and hidden allowed for remixes + await expect( + accessAndSaleModal.locator.getByRole('radio', { disabled: false }) + ).toHaveCount(2) + await accessAndSaleModal.setHidden({ ['Share Button']: true }) + await accessAndSaleModal.save() + + await editPage.openAttributionSettings() + const attributionModal = new AttributionModal(page) + await attributionModal.markAsAIGenerated(aiAttributionName) + await attributionModal.setISRC(isrc) + await attributionModal.setISWC(iswc) + await attributionModal.setAllowAttribution(true) + await attributionModal.setAllowCommercialUse(true) + await attributionModal.setDerivativeWorks('Allowed') + await attributionModal.save() + + await editPage.complete() + + const uploadingPage = new UploadFinishPage(page, 'Track') + await uploadingPage.assertCompletes() + + // Vist track page + await page.getByRole('link', { name: /visit track page/i }).click() + + // Assert title + const trackHeading = page.getByRole('heading', { + name: trackTitle, + level: 1 + }) + await expect(trackHeading).toBeVisible() + + // Assert hidden + const hidden = page.getByRole('heading', { + name: /hidden track/i, + level: 5 + }) + await expect(hidden.first()).toBeVisible() + + // Assert tagged + const tag1 = page.getByRole('link', { name: tags[0] }) + await expect(tag1).toBeVisible() + const tag2 = page.getByRole('link', { name: tags[1] }) + await expect(tag1).toBeVisible() + await expect(tag2).toBeVisible() + + // Assert description + const description = page.getByRole('heading', { + name: trackDescription, + level: 3 + }) + await expect(description).toBeVisible() + + // Assert genre and mood + await expect(page.getByText(genre)).toBeVisible() + await expect(page.getByText(mood)).toBeVisible() + + // Assert shareable + await expect(page.getByRole('button', { name: /share/i })).toBeVisible() + + // Assert remix + await expect(page.getByText('ORIGINAL TRACK')).toBeVisible() + await expect(page.getByRole('link', { name: remixName })).toBeVisible() + + // Assert AI generated + await expect(page.getByText('generated with ai').first()).toBeVisible() + + // Assert ISRC + // TODO + + // Assert ISWC + // TODO + + // Assert license + // TODO +}) + +test('should upload a premium track', async ({ page }) => { + const trackTitle = `Test premium track ${Date.now()}` + const genre = 'Alternative' + const price = '1.05' + const previewSeconds = '15' + + await page.goto('upload') + + const selectPage = new UploadSelectPage(page) + await selectPage.setTracks('track.mp3') + await selectPage.continue() + + const editPage = new EditTrackPage(page) + await editPage.setArtwork('track-artwork.jpeg') + await editPage.setTitle(trackTitle) + await editPage.setGenre(genre) + + await editPage.openAccessAndSaleSettings() + const accessAndSaleModal = new AccessAndSaleModal(page) + await accessAndSaleModal.setPremium({ price, previewSeconds }) + await accessAndSaleModal.save() + + await editPage.complete() + + const uploadingPage = new UploadFinishPage(page, 'Track') + await uploadingPage.assertCompletes() + + // Vist track page + await page.getByRole('link', { name: /visit track page/i }).click() + + // Assert title + const trackHeading = page.getByRole('heading', { + name: trackTitle, + level: 1 + }) + await expect(trackHeading).toBeVisible() + + // Assert premium + const premium = page.getByRole('heading', { + name: /premium track/i, + level: 5 + }) + await expect(premium.first()).toBeVisible() + + // Assert price + await expect(page.getByText('$' + price)).toBeVisible() +}) + +test('should upload a track with free stems', async ({ page }) => { + const trackTitle = `Test stems track ${Date.now()}` + const genre = 'Alternative' + + await page.goto('upload') + + const selectPage = new UploadSelectPage(page) + await selectPage.setTracks('track.mp3') + await selectPage.continue() + + const editPage = new EditTrackPage(page) + await editPage.setArtwork('track-artwork.jpeg') + await editPage.setTitle(trackTitle) + await editPage.setGenre(genre) + + await editPage.openStemsAndDownloadsSettings() + const stemsAndDownloadsModal = new StemsAndDownloadsModal(page) + await stemsAndDownloadsModal.setAllowTrackDownload(true) + await stemsAndDownloadsModal.setStems([ + { filename: 'stem-1.mp3', type: 'Instrumental' }, + { filename: 'stem-2.mp3', type: 'Lead Vocals' } + ]) + await stemsAndDownloadsModal.save() + + await editPage.complete() + + const uploadingPage = new UploadFinishPage(page, 'Track') + await uploadingPage.assertCompletes() + + // Vist track page + await page.getByRole('link', { name: /visit track page/i }).click() + + // Assert title + const trackHeading = page.getByRole('heading', { + name: trackTitle, + level: 1 + }) + await expect(trackHeading).toBeVisible() + + // Assert Stems & Downloads + await page.getByRole('button', { name: /stems & downloads/i }).click() + await expect(page.getByText(/full track/i)).toBeVisible() + await expect(page.getByText(/stem-1.mp3/i)).toBeVisible() + await expect(page.getByText(/instrumental/i)).toBeVisible() + await expect(page.getByText(/stem-2.mp3/i)).toBeVisible() + await expect(page.getByText(/lead vocals/i)).toBeVisible() +}) diff --git a/packages/web/package.json b/packages/web/package.json index cb5f08492c0..05001296b22 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -3,7 +3,7 @@ "productName": "Audius", "description": "The Audius web client reference implementation", "author": "Audius", - "version": "1.5.72", + "version": "1.5.73", "private": true, "scripts": { "DEV & BUILD========================================": "", @@ -31,6 +31,7 @@ "start": "npm run write-sha && npm run publish-scripts && env-cmd ./env/.env.git vite", "stylelint:fix": "npm run stylelint -- --fix", "stylelint": "stylelint 'src/**/*.css'", + "test:e2e": "npx playwright test", "test:coverage": "vitest --resetMocks=false --coverage --watchAll=false", "test": "vitest", "typecheck:watch": "tsc --watch", @@ -148,7 +149,6 @@ "numeral": "2.0.6", "orbit-controls": "0.0.1", "perspective-camera": "2.0.1", - "pixi.js": "4.8.9", "prop-types": "15.7.2", "query-string": "6.13.5", "raw-loader": "0.5.1", @@ -209,6 +209,7 @@ "@electron/notarize": "2.2.0", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@pinata/sdk": "1.1.13", + "@playwright/test": "1.42.1", "@redux-devtools/extension": "3.2.4", "@testing-library/react": "12.0.0", "@types/amplitude-js": "8.0.2", diff --git a/packages/web/playwright.config.ts b/packages/web/playwright.config.ts new file mode 100644 index 00000000000..db7f051d9ed --- /dev/null +++ b/packages/web/playwright.config.ts @@ -0,0 +1,110 @@ +import { defineConfig, devices } from '@playwright/test' +import fs from 'fs' + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +const authFileExists = fs.existsSync('playwright/.auth/user.json') + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI + ? [ + ['junit', { outputFile: 'report.xml', outputDir: 'playwright-report' }], + ['html', { open: 'never' }], + ['line'], + ['blob'] + ] + : 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:3001', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + /* Set the default timeout for actions like "click" */ + actionTimeout: 10 * 1000 + }, + + /* Total timeout for individual tests */ + timeout: 5 * 60 * 1000, + + /* Timeout for each assertion */ + expect: { + timeout: 10 * 1000 + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'setup', + testMatch: /.*\.setup.ts/ + }, + { + name: 'chromium', + dependencies: authFileExists ? undefined : ['setup'], + testIgnore: /.*\.setup.ts/, + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/user.json' + } + } + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npm run start:spa:stage', + url: 'http://localhost:3001', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + timeout: 60000 * 5 + } +}) diff --git a/packages/web/src/app/web-player/WebPlayer.jsx b/packages/web/src/app/web-player/WebPlayer.jsx index 74f7a10f6c9..c1ee135a583 100644 --- a/packages/web/src/app/web-player/WebPlayer.jsx +++ b/packages/web/src/app/web-player/WebPlayer.jsx @@ -484,6 +484,10 @@ class WebPlayer extends Component { {this.props.isChatEnabled ? : null} + + {/* This is used in E2E tests to determine that client-side JS is loaded */} +
+
{this.props.showCookieBanner ? : null} @@ -991,7 +995,9 @@ class WebPlayer extends Component {
- + + + diff --git a/packages/web/src/assets/styles/colors.css b/packages/web/src/assets/styles/colors.css index bb248e4ad2f..e1e9cbdea81 100644 --- a/packages/web/src/assets/styles/colors.css +++ b/packages/web/src/assets/styles/colors.css @@ -97,25 +97,39 @@ --next-and-previous-buttons: var(--harmony-neutral); --segmented-progress-bar-segment-1: linear-gradient( - 180deg; #ed75f9 0%; #cc0fe0 100% + 180deg, + #ed75f9 0%, + #cc0fe0 100% ); --segmented-progress-bar-segment-2: linear-gradient( - 180deg; #e477fb 0%; #b815db 100% + 180deg, + #e477fb 0%, + #b815db 100% ); --segmented-progress-bar-segment-3: linear-gradient( - 180deg; #d069f5 0%; #9f0ad4 100% + 180deg, + #d069f5 0%, + #9f0ad4 100% ); --segmented-progress-bar-segment-4: linear-gradient( - 180deg; #cc75fa 0%; #8908ce 100% + 180deg, + #cc75fa 0%, + #8908ce 100% ); --segmented-progress-bar-segment-5: linear-gradient( - 180deg; #af4dff 0%; #7305c8 100% + 180deg, + #af4dff 0%, + #7305c8 100% ); --segmented-progress-bar-segment-6: linear-gradient( - 180deg; #9c45ff 0%; #5c03c2 100% + 180deg, + #9c45ff 0%, + #5c03c2 100% ); --segmented-progress-bar-segment-7: linear-gradient( - 180deg; #833bfb 0%; #4600bc 100% + 180deg, + #833bfb 0%, + #4600bc 100% ); } diff --git a/packages/web/src/common/store/pages/signon/actions.ts b/packages/web/src/common/store/pages/signon/actions.ts index 42b5441d1d7..a5580c13dbd 100644 --- a/packages/web/src/common/store/pages/signon/actions.ts +++ b/packages/web/src/common/store/pages/signon/actions.ts @@ -3,7 +3,8 @@ import { InstagramProfile, TwitterProfile, TikTokProfile, - Image + Image, + toastActions } from '@audius/common/store' import { createCustomAction } from 'typesafe-actions' @@ -65,7 +66,6 @@ export const NEXT_PAGE = 'SIGN_ON/NEXT_PAGE' export const PREVIOUS_PAGE = 'SIGN_ON/PREVIOUS_PAGE' export const GO_TO_PAGE = 'SIGN_ON/GO_TO_PAGE' -export const SET_TOAST = 'SIGN_ON/SET_TOAST' export const UPDATE_ROUTE_ON_COMPLETION = 'SIGN_ON/UPDATE_ROUTE_ON_COMPLETION' export const UPDATE_ROUTE_ON_EXIT = 'SIGN_ON/UPDATE_ROUTE_ON_EXIT' @@ -421,26 +421,10 @@ export function removeFollowArtists(userIds: ID[]) { return { type: REMOVE_FOLLOW_ARTISTS, userIds } } -/** - * Sets a toast appearing over signon - */ -export const showToast = (text: string) => ({ - type: SET_TOAST, - text -}) - -/** - * Clears a toast appearing over signon - */ -export const clearToast = () => ({ - type: SET_TOAST, - text: null -}) - -export const showRequiresAccountModal = () => ({ - type: SET_TOAST, - text: 'Oops, it looks like you need an account to do that!' -}) +export const showRequiresAccountModal = () => + toastActions.toast({ + content: 'Oops, it looks like you need an account to do that!' + }) /** * Opens the signin/up modal in mobile and routes to the page on desktop diff --git a/packages/web/src/common/store/pages/signon/reducer.js b/packages/web/src/common/store/pages/signon/reducer.js index 398fb75df41..596a30aa936 100644 --- a/packages/web/src/common/store/pages/signon/reducer.js +++ b/packages/web/src/common/store/pages/signon/reducer.js @@ -31,7 +31,6 @@ import { SIGN_IN_FAILED, SIGN_IN_SUCCEEDED, CONFIGURE_META_MASK, - SET_TOAST, UPDATE_ROUTE_ON_COMPLETION, UPDATE_ROUTE_ON_EXIT, ADD_FOLLOW_ARTISTS, @@ -74,7 +73,6 @@ const initialState = { profileImage: null, // Object with file blob & url coverPhoto: null, // Object with file blob & url status: 'editing', // 'editing', 'loading', 'success', or 'failure' - toastText: null, page: Pages.EMAIL, startedSignUpProcess: false, /** @deprecated */ @@ -409,12 +407,6 @@ const actionsMap = { otp: createTextField() } }, - [SET_TOAST](state, action) { - return { - ...state, - toastText: action.text - } - }, [UPDATE_ROUTE_ON_COMPLETION](state, action) { return { ...state, diff --git a/packages/web/src/common/store/pages/signon/sagas.ts b/packages/web/src/common/store/pages/signon/sagas.ts index 5386125d602..097d8098aef 100644 --- a/packages/web/src/common/store/pages/signon/sagas.ts +++ b/packages/web/src/common/store/pages/signon/sagas.ts @@ -813,7 +813,7 @@ function* signIn(action: ReturnType) { } }) ) - yield* put(signOnActions.showToast(messages.incompleteAccount)) + yield* put(toastActions.toast({ content: messages.incompleteAccount })) const trackEvent = make(Name.SIGN_IN_WITH_INCOMPLETE_ACCOUNT, { handle: signInResponse.handle @@ -826,7 +826,7 @@ function* signIn(action: ReturnType) { accountAlreadyExisted: true }) ) - yield* put(signOnActions.showToast(messages.incompleteAccount)) + yield* put(toastActions.toast({ content: messages.incompleteAccount })) } else { yield* put( signOnActions.signInFailed( @@ -1038,18 +1038,6 @@ function* watchFollowArtists() { yield* takeLatest(signOnActions.FOLLOW_ARTISTS, followArtists) } -function* watchShowToast() { - yield* takeLatest( - signOnActions.SET_TOAST, - function* (action: ReturnType) { - if (action.text) { - yield* delay(5000) - yield* put(signOnActions.clearToast()) - } - } - ) -} - function* watchOpenSignOn() { yield* takeLatest( signOnActions.OPEN_SIGN_ON, @@ -1085,7 +1073,6 @@ export default function sagas() { watchFollowArtists, watchGetArtistsToFollow, watchConfigureMetaMask, - watchShowToast, watchOpenSignOn, watchSignOnError, watchSendWelcomeEmail, diff --git a/packages/web/src/common/store/pages/signon/selectors.ts b/packages/web/src/common/store/pages/signon/selectors.ts index c3840040097..18fc5a63503 100644 --- a/packages/web/src/common/store/pages/signon/selectors.ts +++ b/packages/web/src/common/store/pages/signon/selectors.ts @@ -31,7 +31,6 @@ export const getIsMobileSignOnVisible = (state: AppState) => state.signOn.isMobileSignOnVisible export const getStatus = (state: AppState) => state.signOn.status export const getPage = (state: AppState) => state.signOn.page -export const getToastText = (state: AppState) => state.signOn.toastText export const getRouteOnCompletion = (state: AppState) => state.signOn.routeOnCompletion export const getRouteOnExit = (state: AppState) => state.signOn.routeOnExit diff --git a/packages/web/src/common/store/pages/signon/types.ts b/packages/web/src/common/store/pages/signon/types.ts index c4709562756..4e51a34a007 100644 --- a/packages/web/src/common/store/pages/signon/types.ts +++ b/packages/web/src/common/store/pages/signon/types.ts @@ -64,7 +64,6 @@ export default interface SignOnPageState { suggestedFollowEntries: User[] followIds: ID[] status: EditingStatus - toastText: string | null hidePreviewHint: boolean followArtists: FollowArtists isMobileSignOnVisible: boolean diff --git a/packages/web/src/common/utils/isMobileWeb.ts b/packages/web/src/common/utils/isMobileWeb.ts index 74976277d84..2bc0f0f4c5d 100644 --- a/packages/web/src/common/utils/isMobileWeb.ts +++ b/packages/web/src/common/utils/isMobileWeb.ts @@ -20,8 +20,5 @@ export const isMobileWeb = () => { if (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) check = true - if (navigator.userAgent === 'probers' && window.innerWidth < 500) { - check = true - } return check } diff --git a/packages/web/src/components/animated-button/AnimatedButtonProvider.tsx b/packages/web/src/components/animated-button/AnimatedButtonProvider.tsx index 8b569be428f..4fc5ff3d4da 100644 --- a/packages/web/src/components/animated-button/AnimatedButtonProvider.tsx +++ b/packages/web/src/components/animated-button/AnimatedButtonProvider.tsx @@ -51,7 +51,9 @@ const AnimatedButton = ({ disabledClassName = '', wrapperClassName = '', stopPropagation = false, - disableAnimationOnMount = true + disableAnimationOnMount = true, + uniqueKey, + ...buttonProps }: AnimatedButtonProps) => { const [isPaused, setIsPaused] = useState(true) const [getDidMount, setDidMount] = useInstanceVar(false) @@ -122,7 +124,7 @@ const AnimatedButton = ({ } return href ? ( - + {buttonElement} ) : ( diff --git a/packages/web/src/components/app-redirect-popover/components/AppRedirectPopover.tsx b/packages/web/src/components/app-redirect-popover/components/AppRedirectPopover.tsx index f3a368e5a4a..369d3105e73 100644 --- a/packages/web/src/components/app-redirect-popover/components/AppRedirectPopover.tsx +++ b/packages/web/src/components/app-redirect-popover/components/AppRedirectPopover.tsx @@ -109,8 +109,7 @@ export const AppRedirectPopover = (props: AppRedirectPopoverProps) => { !matchPath(history.location.pathname, { path: '/', exact: true }) && animDelay && !isDismissed && - isMobile && - !(navigator.userAgent === 'probers') + isMobile useEffect(() => { shouldShow && incrementScroll() @@ -194,7 +193,7 @@ export const AppRedirectPopover = (props: AppRedirectPopoverProps) => { style={props} > )}
)} - - - Done - + + + Done + + diff --git a/packages/web/src/components/cover-photo/CoverPhoto.module.css b/packages/web/src/components/cover-photo/CoverPhoto.module.css index a2ff8a47bb0..b73efe43254 100644 --- a/packages/web/src/components/cover-photo/CoverPhoto.module.css +++ b/packages/web/src/components/cover-photo/CoverPhoto.module.css @@ -1,19 +1,21 @@ .coverPhoto { width: 100%; - height: 100%; - min-height: 376px; + height: 376px; position: relative; display: flex; justify-content: center; + overflow: hidden; } .placeholder, .photo { position: absolute; width: 100%; + height: 100%; min-height: 376px; display: flex; background-position: center; + z-index: 0; } .photo { @@ -47,6 +49,7 @@ .hide { opacity: 0; } + .show { opacity: 1; } diff --git a/packages/web/src/components/cover-photo/CoverPhoto.tsx b/packages/web/src/components/cover-photo/CoverPhoto.tsx index 43f397aa9bd..9467524db9b 100644 --- a/packages/web/src/components/cover-photo/CoverPhoto.tsx +++ b/packages/web/src/components/cover-photo/CoverPhoto.tsx @@ -104,25 +104,27 @@ const CoverPhoto = ({ isUrl={false} wrapperClassName={styles.photo} imageStyle={backgroundStyle} + useBlur={shouldBlur} usePlaceholder={false} immediate={immediate} - useBlur={shouldBlur} >
{processing ? loadingElement : null}
-
- {edit ? ( - - ) : null} -
+ +
+ {edit ? ( + + ) : null} +
+
) } diff --git a/packages/web/src/components/data-entry/AccessAndSaleTriggerLegacy.tsx b/packages/web/src/components/data-entry/AccessAndSaleTriggerLegacy.tsx index b28fe28b6ea..3b3f4f966c6 100644 --- a/packages/web/src/components/data-entry/AccessAndSaleTriggerLegacy.tsx +++ b/packages/web/src/components/data-entry/AccessAndSaleTriggerLegacy.tsx @@ -370,7 +370,7 @@ export const AccessAndSaleTriggerLegacy = ( renderValue={() => null} previewOverride={(toggleMenu) => ( ) } diff --git a/packages/web/src/components/profile-page-nav-section-title/ProfilePageNavSectionTitle.tsx b/packages/web/src/components/profile-page-nav-section-title/ProfilePageNavSectionTitle.tsx index 09945fe48df..328d47d84df 100644 --- a/packages/web/src/components/profile-page-nav-section-title/ProfilePageNavSectionTitle.tsx +++ b/packages/web/src/components/profile-page-nav-section-title/ProfilePageNavSectionTitle.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react' +import { ReactNode } from 'react' import styles from './ProfilePageNavSectionTitle.module.css' diff --git a/packages/web/src/components/profile-picture/ProfilePicture.jsx b/packages/web/src/components/profile-picture/ProfilePicture.jsx index e4ad4c92ccb..3fd4560f1bc 100644 --- a/packages/web/src/components/profile-picture/ProfilePicture.jsx +++ b/packages/web/src/components/profile-picture/ProfilePicture.jsx @@ -1,17 +1,24 @@ import { memo, useState, useEffect } from 'react' +import { imageProfilePicEmpty } from '@audius/common/assets' import { SquareSizes } from '@audius/common/models' +import { cacheUsersSelectors } from '@audius/common/store' import cn from 'classnames' import PropTypes from 'prop-types' import Lottie from 'react-lottie' +import { useSelector } from 'react-redux' import loadingSpinner from 'assets/animations/loadingSpinner.json' import DynamicImage from 'components/dynamic-image/DynamicImage' import ImageSelectionButton from 'components/image-selection/ImageSelectionButton' +import { StaticImage } from 'components/static-image/StaticImage' import { useUserProfilePicture } from 'hooks/useUserProfilePicture' +import { useSsrContext } from 'ssr/SsrContext' import styles from './ProfilePicture.module.css' +const { getUser } = cacheUsersSelectors + const ProfilePicture = ({ editMode, userId, @@ -34,6 +41,8 @@ const ProfilePicture = ({ const [hasChanged, setHasChanged] = useState(false) const [processing, setProcessing] = useState(false) const [modalOpen, setModalOpen] = useState(false) + const user = useSelector((state) => getUser(state, { id: userId })) + const { isSsrEnabled } = useSsrContext() useEffect(() => { if (editMode) { @@ -57,6 +66,8 @@ const ProfilePicture = ({ setModalOpen(false) } + const ImageElement = isSsrEnabled ? StaticImage : DynamicImage + return (
-
)} - + {(editMode || showEdit) && ( 0 ) { - if (!nextState.selected && !nextState.valueFromParent) { - this.setState({ open: true }) - } if (nextProps.status === Status.SUCCESS) { return true } @@ -236,9 +233,6 @@ class SearchBar extends Component { nextProps.resultsCount === 0 && this.state.value !== '' ) { - if (!nextState.selected && !nextState.valueFromParent) { - this.setState({ open: true }) - } return false } // Close the dropdown if we're searching for '' (deleted text in search). diff --git a/packages/web/src/components/share-modal/components/ShareDialog.tsx b/packages/web/src/components/share-modal/components/ShareDialog.tsx index f531a73efb1..99ad0b92093 100644 --- a/packages/web/src/components/share-modal/components/ShareDialog.tsx +++ b/packages/web/src/components/share-modal/components/ShareDialog.tsx @@ -24,7 +24,7 @@ const ShareActionListItem = (props: ShareActionListItemProps) => { return (
  • ) : null} diff --git a/packages/web/src/components/static-image/StaticImage.tsx b/packages/web/src/components/static-image/StaticImage.tsx index b53afac50ac..f2b1b4d69f4 100644 --- a/packages/web/src/components/static-image/StaticImage.tsx +++ b/packages/web/src/components/static-image/StaticImage.tsx @@ -1,6 +1,6 @@ import { ComponentPropsWithoutRef } from 'react' -import { SquareSizes } from '@audius/common/models' +import { WidthSizes, SquareSizes } from '@audius/common/models' import { Box, Flex } from '@audius/harmony' import { DiscoveryNodeSelector, @@ -17,11 +17,15 @@ export type StaticImageProps = { // Image CID cid?: string | null // Size of image to select - size?: SquareSizes + size?: SquareSizes | WidthSizes + // Override image url. Used for updated profile picture and things + imageUrl?: string // Set height and width of wrapper container to 100% fullWidth?: boolean // Classes to apply to the wrapper wrapperClassName?: string + // Whether or not to blur the background image + useBlur?: boolean } & ComponentPropsWithoutRef<'img'> const sdkConfigs = { @@ -50,9 +54,11 @@ export const StaticImage = (props: StaticImageProps) => { const { cid, size = SquareSizes.SIZE_1000_BY_1000, + imageUrl, fullWidth = false, wrapperClassName, children, + useBlur, ...other } = props @@ -70,14 +76,35 @@ export const StaticImage = (props: StaticImageProps) => { className={wrapperClassName} css={{ overflow: 'hidden' }} > - + + + {useBlur ? ( + + ) : null} {children ? ( {children} diff --git a/packages/web/src/components/subscribe-button/SubscribeButton.module.css b/packages/web/src/components/subscribe-button/SubscribeButton.module.css deleted file mode 100644 index 2cec016fea6..00000000000 --- a/packages/web/src/components/subscribe-button/SubscribeButton.module.css +++ /dev/null @@ -1,67 +0,0 @@ -.container { - opacity: 1; - height: 32px; - width: 32px; - border: 1px solid var(--harmony-n-200); - border-radius: 4px; - background-color: var(--harmony-white); - display: flex; - justify-content: center; - align-items: center; - transition: all ease-in-out 0.07s; - cursor: pointer; -} - -.container:not(.isMobile):hover { - border: 1px solid var(--harmony-p-500); - background-color: var(--harmony-primary); -} - -.container:active { - border: 1px solid var(--harmony-p-500); - background-color: var(--harmony-p-300); -} - -.container.isSubscribed { - border: none; - background-color: var(--harmony-primary); -} - -.container.isSubscribed:hover { - border: 1px solid var(--harmony-p-500); - background-color: var(--harmony-primary); -} - -.container:not(.isMobile).isSubscribed:active { - border: 1px solid var(--harmony-p-500); - background-color: var(--harmony-p-300); -} - -.icon { - height: 20px; - width: 20px; -} - -.icon path { - fill: var(--harmony-neutral); - transition: all ease-in-out 0.07s; -} - -.container.isSubscribed .icon path, -.container:active .icon path, -.container:not(.isMobile):hover .icon path { - fill: var(--harmony-static-white); -} - -.container.isMatrix.isSubscribed .icon path, -.container.isMatrix:active .icon path, -.container.isMatrix:not(.isMobile):hover .icon path { - fill: var(--darkmode-static-white); -} - -.container.notFollowing { - opacity: 0; - width: 0%; - border: 0px; - margin: 0px; -} diff --git a/packages/web/src/components/subscribe-button/SubscribeButton.tsx b/packages/web/src/components/subscribe-button/SubscribeButton.tsx index d0e05b07949..ed96e31bdd1 100644 --- a/packages/web/src/components/subscribe-button/SubscribeButton.tsx +++ b/packages/web/src/components/subscribe-button/SubscribeButton.tsx @@ -1,32 +1,21 @@ import { useState, useCallback, useEffect } from 'react' import { + Button, IconNotificationOn as IconNotification, IconNotificationOff } from '@audius/harmony' -import cn from 'classnames' - -import { useIsMobile } from 'hooks/useIsMobile' -import { isMatrix } from 'utils/theme/theme' - -import styles from './SubscribeButton.module.css' type SubscribeButtonProps = { - className?: string isSubscribed: boolean isFollowing: boolean onToggleSubscribe: () => void } -const SubscribeButton = ({ - className, - isFollowing, - isSubscribed, - onToggleSubscribe -}: SubscribeButtonProps) => { +const SubscribeButton = (props: SubscribeButtonProps) => { + const { isFollowing, isSubscribed, onToggleSubscribe } = props const [isHovering, setIsHovering] = useState(false) const [isHoveringClicked, setIsHoveringClicked] = useState(false) - const isMobile = useIsMobile() const onClick = useCallback(() => { onToggleSubscribe() setIsHoveringClicked(true) @@ -36,26 +25,21 @@ const SubscribeButton = ({ if (!isHovering && isHoveringClicked) setIsHoveringClicked(false) }, [isHovering, isHoveringClicked, setIsHoveringClicked]) + if (!isFollowing) return null + + const showNotificationOff = + (isHovering && isSubscribed && !isHoveringClicked) || + (isHovering && !isSubscribed && isHoveringClicked) + return ( -
    setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} + size='small' + iconLeft={showNotificationOff ? IconNotificationOff : IconNotification} onClick={onClick} - > - {(isHovering && isSubscribed && !isHoveringClicked) || - (isHovering && !isSubscribed && isHoveringClicked) ? ( - - ) : ( - - )} -
    + /> ) } diff --git a/packages/web/src/components/tipping/support/SupportingList.module.css b/packages/web/src/components/tipping/support/SupportingList.module.css deleted file mode 100644 index 6d96f437969..00000000000 --- a/packages/web/src/components/tipping/support/SupportingList.module.css +++ /dev/null @@ -1,31 +0,0 @@ -.container { - margin-top: 32px; -} - -.tile { - margin-top: 16px; -} - -.tipIcon { - width: 18px; - height: 18px; -} -.tipIcon path { - fill: var(--harmony-neutral); -} - -.seeMore { - display: flex; - align-items: center; - margin-top: 16px; - font-weight: var(--harmony-font-bold); - font-size: var(--harmony-font-s); - color: var(--harmony-n-400); -} -.seeMore:hover { - cursor: pointer; - transform: scale3d(1.01, 1.01, 1.01); -} -.seeMore:active { - transform: scale3d(0.99, 0.99, 0.99); -} diff --git a/packages/web/src/components/tipping/support/SupportingList.tsx b/packages/web/src/components/tipping/support/SupportingList.tsx index 141c2321d83..4c33899130d 100644 --- a/packages/web/src/components/tipping/support/SupportingList.tsx +++ b/packages/web/src/components/tipping/support/SupportingList.tsx @@ -4,10 +4,7 @@ import { useRankedSupportingForUser } from '@audius/common/hooks' import { User } from '@audius/common/models' import { profilePageSelectors } from '@audius/common/store' import { formatCount, MAX_PROFILE_SUPPORTING_TILES } from '@audius/common/utils' -import { - IconTipping as IconTip, - IconArrowRight as IconArrow -} from '@audius/harmony' +import { IconTipping, IconArrowRight, PlainButton, Flex } from '@audius/harmony' import { useDispatch } from 'react-redux' import { useSelector } from 'common/hooks/useSelector' @@ -21,7 +18,6 @@ import { UserListType } from 'store/application/ui/userListModal/types' -import styles from './SupportingList.module.css' import { SupportingTile } from './SupportingTile' const { getProfileUser } = profilePageSelectors @@ -38,7 +34,7 @@ const SupportingListForProfile = ({ profile }: { profile: User }) => { const dispatch = useDispatch() const rankedSupportingList = useRankedSupportingForUser(profile.user_id) - const handleClick = useCallback(() => { + const handleClickSeeMore = useCallback(() => { if (profile) { dispatch( setUsers({ @@ -52,25 +48,26 @@ const SupportingListForProfile = ({ profile }: { profile: User }) => { }, [profile, dispatch]) return profile && rankedSupportingList.length > 0 ? ( -
    + } + titleIcon={} /> {rankedSupportingList .slice(0, MAX_PROFILE_SUPPORTING_TILES) - .map((supporting, index) => ( -
    - -
    + .map((supporting) => ( + ))} - {profile.supporting_count > MAX_PROFILE_SUPPORTING_TILES && ( -
    - {formatViewAllMessage(profile.supporting_count)} - -
    - )} -
    + {profile.supporting_count > MAX_PROFILE_SUPPORTING_TILES ? ( + + {formatViewAllMessage(profile.supporting_count)} + + ) : null} + ) : null } diff --git a/packages/web/src/components/tipping/tip-audio/SendTip.tsx b/packages/web/src/components/tipping/tip-audio/SendTip.tsx index 385915833bf..9c1fab40b2b 100644 --- a/packages/web/src/components/tipping/tip-audio/SendTip.tsx +++ b/packages/web/src/components/tipping/tip-audio/SendTip.tsx @@ -31,7 +31,8 @@ import { IconTrophy, TokenAmountInput, TokenAmountInputChangeHandler, - Button + Button, + IconComponent } from '@audius/harmony' import BN from 'bn.js' import cn from 'classnames' @@ -69,12 +70,20 @@ const messages = { buyAudioPrefix: 'Buy $AUDIO using ' } -const TopBanner = ({ icon, text }: { icon?: ReactNode; text: ReactNode }) => ( -
    - {icon ? {icon} : null} - {text} -
    -) +type TopBannerProps = { + icon?: IconComponent + text: ReactNode +} + +const TopBanner = (props: TopBannerProps) => { + const { icon: Icon, text } = props + return ( +
    + {Icon ? : null} + {text} +
    + ) +} export const SendTip = () => { const dispatch = useDispatch() @@ -192,10 +201,10 @@ export const SendTip = () => { const topBanner = audioFeaturesDegradedText ? ( ) : !hasInsufficientBalance && isFirstSupporter ? ( - } text={messages.becomeFirstSupporter} /> + ) : !hasInsufficientBalance && amountToTipToBecomeTopSupporter ? ( } + icon={IconTrophy} text={ <> {messages.becomeTopSupporterPrefix} @@ -211,8 +220,9 @@ export const SendTip = () => { ({ + paddingVertical: theme.spacing.s + })} onClick={handleBuyWithStripeClicked} /> diff --git a/packages/web/src/components/tipping/tip-audio/TipAudio.module.css b/packages/web/src/components/tipping/tip-audio/TipAudio.module.css index 1a1fe5b4fb9..44718312eab 100644 --- a/packages/web/src/components/tipping/tip-audio/TipAudio.module.css +++ b/packages/web/src/components/tipping/tip-audio/TipAudio.module.css @@ -78,35 +78,10 @@ border-radius: 4px; } -.buyAudioButton { - width: 100%; - height: unset !important; - padding: 8px 0; -} -.buyAudioButton > .buyAudioButtonText { - font-size: var(--harmony-font-m); -} -.buyAudioButton:hover:enabled { - transform: scale3d(1.02, 1.02, 1.02) !important; -} -.buyAudioButton:active:enabled { - transform: scale3d(0.98, 0.98, 0.98) !important; -} - .divider { border-bottom: 1px solid var(--harmony-n-100); } -.topBannerIcon svg { - width: 2em; - height: 2em; - margin-top: 2px; -} - -.topBannerIcon svg path { - fill: var(--harmony-white); -} - .topBannerText { padding: 8px 0; flex: 1; diff --git a/packages/web/src/components/tipping/tip-audio/TipAudioButton.tsx b/packages/web/src/components/tipping/tip-audio/TipAudioButton.tsx index 1d850b6e715..cc36cef7d0f 100644 --- a/packages/web/src/components/tipping/tip-audio/TipAudioButton.tsx +++ b/packages/web/src/components/tipping/tip-audio/TipAudioButton.tsx @@ -23,8 +23,9 @@ export const TipAudioButton = () => { return ( diff --git a/packages/web/src/pages/audio-rewards-page/TrendingRewardsTile.tsx b/packages/web/src/pages/audio-rewards-page/TrendingRewardsTile.tsx index d71b8e46848..32df62712da 100644 --- a/packages/web/src/pages/audio-rewards-page/TrendingRewardsTile.tsx +++ b/packages/web/src/pages/audio-rewards-page/TrendingRewardsTile.tsx @@ -50,7 +50,7 @@ const RewardPanel = ({ {description()} - - {!isMobile && isTransactionsEnabled && ( - - )} - + {isMobile && ( )} @@ -217,7 +218,6 @@ const OnRampTooltipButton = ({
    diff --git a/packages/web/src/pages/audio-rewards-page/components/modals/VipDiscordModal.tsx b/packages/web/src/pages/audio-rewards-page/components/modals/VipDiscordModal.tsx index 13965fc32ef..b3987f92f32 100644 --- a/packages/web/src/pages/audio-rewards-page/components/modals/VipDiscordModal.tsx +++ b/packages/web/src/pages/audio-rewards-page/components/modals/VipDiscordModal.tsx @@ -65,7 +65,7 @@ export const VipDiscordModal = () => { ) : null} ) : null diff --git a/packages/web/src/pages/pay-and-earn-page/components/USDCCard.module.css b/packages/web/src/pages/pay-and-earn-page/components/USDCCard.module.css index 8b8715d21cd..60ca5c0ebf2 100644 --- a/packages/web/src/pages/pay-and-earn-page/components/USDCCard.module.css +++ b/packages/web/src/pages/pay-and-earn-page/components/USDCCard.module.css @@ -1,12 +1,3 @@ -.usdcContainer { - width: 100%; - flex-direction: column; - border-radius: var(--harmony-unit-3); - background: var(--harmony-white); - overflow: hidden; - box-shadow: var(--table-shadow) 0px 16px 20px 0px; -} - .backgroundBlueGradient { display: flex; width: 100%; diff --git a/packages/web/src/pages/pay-and-earn-page/components/USDCCard.tsx b/packages/web/src/pages/pay-and-earn-page/components/USDCCard.tsx index 46f69a5bd8e..a20ad98ca15 100644 --- a/packages/web/src/pages/pay-and-earn-page/components/USDCCard.tsx +++ b/packages/web/src/pages/pay-and-earn-page/components/USDCCard.tsx @@ -17,7 +17,8 @@ import { IconQuestionCircle, Flex, IconLogoCircleUSDC as LogoUSDC, - Text + Text, + Paper } from '@audius/harmony' import BN from 'bn.js' @@ -76,11 +77,14 @@ export const USDCCard = () => { } return ( -
    +
    - + ({ path: { fill: theme.color.static.white } })} + />
    {
    -
    + ) } diff --git a/packages/web/src/pages/profile-page/components/EditableName.jsx b/packages/web/src/pages/profile-page/components/EditableName.jsx deleted file mode 100644 index c09e934bea2..00000000000 --- a/packages/web/src/pages/profile-page/components/EditableName.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import { memo, useState, useRef, useEffect } from 'react' - -import { IconPencil } from '@audius/harmony' -import cn from 'classnames' - -import UserBadges from 'components/user-badges/UserBadges' - -import styles from './EditableName.module.css' - -const EditableName = (props) => { - const [editing, setEditing] = useState(false) - const inputRef = useRef(null) - useEffect(() => { - if (editing) inputRef.current.focus() - }) - - const onInputBlur = () => { - setEditing(false) - props.onChange(inputRef.current.value) - } - - return ( -
    - {props.editable ? ( - editing ? ( - <> - - - ) : ( -
    - {props.name} - { - setEditing(true)} - > - - - } -
    - ) - ) : ( - <> -

    {props.name}

    - - - )} -
    - ) -} - -export default memo(EditableName) diff --git a/packages/web/src/pages/profile-page/components/EditableName.module.css b/packages/web/src/pages/profile-page/components/EditableName.module.css index 15337510701..fafdf23c05b 100644 --- a/packages/web/src/pages/profile-page/components/EditableName.module.css +++ b/packages/web/src/pages/profile-page/components/EditableName.module.css @@ -33,10 +33,6 @@ .iconVerified { margin-left: 4px; } -.name > .editNameContainer > .iconPencil { - cursor: pointer; -} -.name > .editNameContainer > .iconPencil svg, .name > .iconVerified svg { margin-left: 8px; filter: drop-shadow(0px 1px 4px rgba(0, 0, 0, 0.7)); diff --git a/packages/web/src/pages/profile-page/components/EditableName.tsx b/packages/web/src/pages/profile-page/components/EditableName.tsx new file mode 100644 index 00000000000..740264af4c0 --- /dev/null +++ b/packages/web/src/pages/profile-page/components/EditableName.tsx @@ -0,0 +1,75 @@ +import { useState, useRef, useEffect } from 'react' + +import { IconButton, IconPencil } from '@audius/harmony' +import cn from 'classnames' + +import UserBadges from 'components/user-badges/UserBadges' + +import styles from './EditableName.module.css' + +const messages = { + editLabel: 'Edit Name' +} + +type EditableNameProps = { + name: string + userId: number + onChange: (name: string) => void + editable: boolean + className?: string + verified?: boolean +} + +export const EditableName = (props: EditableNameProps) => { + const { name, userId, onChange, editable, className, verified } = props + const [editing, setEditing] = useState(false) + const inputRef = useRef(null) + useEffect(() => { + if (editing) inputRef.current?.focus() + }) + + const onInputBlur = () => { + setEditing(false) + onChange(inputRef.current?.value as string) + } + + return ( +
    + {editable ? ( + editing ? ( + <> + + + ) : ( +
    + {name} + ({ marginBottom: theme.spacing.s })} + aria-label={messages.editLabel} + icon={IconPencil} + color='staticWhite' + onClick={() => setEditing(true)} + shadow='drop' + /> +
    + ) + ) : ( + <> +

    {name}

    + + + )} +
    + ) +} diff --git a/packages/web/src/pages/profile-page/components/SocialLink.module.css b/packages/web/src/pages/profile-page/components/SocialLink.module.css deleted file mode 100644 index 64af9eb7132..00000000000 --- a/packages/web/src/pages/profile-page/components/SocialLink.module.css +++ /dev/null @@ -1,18 +0,0 @@ -.root { - display: inline-flex; - column-gap: var(--harmony-unit-2); -} - -.root:hover .icon path { - fill: var(--harmony-primary); -} - -.text { - max-width: 180px; - text-overflow: ellipsis; - overflow-x: hidden; -} - -.singleLink { - white-space: nowrap; -} diff --git a/packages/web/src/pages/profile-page/components/SocialLink.tsx b/packages/web/src/pages/profile-page/components/SocialLink.tsx index 1380087f7f5..18234771218 100644 --- a/packages/web/src/pages/profile-page/components/SocialLink.tsx +++ b/packages/web/src/pages/profile-page/components/SocialLink.tsx @@ -6,16 +6,14 @@ import { IconTwitter as IconTwitterBird, IconInstagram, IconDonate, - Text + Text, + Flex } from '@audius/harmony' -import cn from 'classnames' import { ExternalLink } from 'components/link' import Tooltip from 'components/tooltip/Tooltip' import { UserGeneratedText } from 'components/user-generated-text' -import styles from './SocialLink.module.css' - export enum Type { TWITTER, INSTAGRAM, @@ -65,13 +63,7 @@ const SocialLink = (props: SocialLinkProps) => { const SocialIcon = socialIcons[type] - let icon = ( - - ) + let icon = if (type === Type.DONATION) { icon = {icon} } @@ -98,17 +90,19 @@ const SocialLink = (props: SocialLinkProps) => { } } - const Root = href ? ExternalLink : Text + if (href) { + return ( + + {icon} {iconOnly ? null : text} + + ) + } return ( - + {icon} - {iconOnly ? null : ( - - {text} - - )} - + {iconOnly ? null : text} + ) } diff --git a/packages/web/src/pages/profile-page/components/desktop/ProfileBio.tsx b/packages/web/src/pages/profile-page/components/desktop/ProfileBio.tsx index da474036930..9d6cb738d81 100644 --- a/packages/web/src/pages/profile-page/components/desktop/ProfileBio.tsx +++ b/packages/web/src/pages/profile-page/components/desktop/ProfileBio.tsx @@ -1,10 +1,7 @@ import { useCallback, useEffect, useState } from 'react' import { Name } from '@audius/common/models' -import { - IconCaretDown as IconCaretDownLine, - IconCaretUp as IconCaretUpLine -} from '@audius/harmony' +import { IconCaretDown, IconCaretUp, PlainButton } from '@audius/harmony' import { ResizeObserver } from '@juggle/resize-observer' import cn from 'classnames' // eslint-disable-next-line no-restricted-imports -- TODO: migrate to @react-spring/web @@ -232,31 +229,22 @@ export const ProfileBio = ({ > {bio} - {isCollapsed ? ( -
    - -
    - {messages.seeMore} - -
    -
    - ) : ( -
    - - {isCollapsible ? ( -
    - {messages.seeLess} - -
    - ) : null} -
    - )} +
    + + ({ + marginTop: theme.spacing.l, + paddingLeft: 0 + })} + > + {isCollapsed ? messages.seeMore : messages.seeLess} + +
    ) } diff --git a/packages/web/src/pages/profile-page/components/desktop/ProfileLeftNav.tsx b/packages/web/src/pages/profile-page/components/desktop/ProfileLeftNav.tsx index 280bc75914a..abde7d61fc7 100644 --- a/packages/web/src/pages/profile-page/components/desktop/ProfileLeftNav.tsx +++ b/packages/web/src/pages/profile-page/components/desktop/ProfileLeftNav.tsx @@ -6,6 +6,7 @@ import { animated } from 'react-spring' import { useSelector } from 'common/hooks/useSelector' import { AiGeneratedCallout } from 'components/ai-generated-button/AiGeneratedCallout' +import { ClientOnly } from 'components/client-only/ClientOnly' import Input from 'components/data-entry/Input' import TextArea from 'components/data-entry/TextArea' import { RelatedArtists } from 'components/related-artists/RelatedArtists' @@ -18,6 +19,7 @@ import ProfilePageBadge from 'components/user-badges/ProfilePageBadge' import { Type } from 'pages/profile-page/components/SocialLink' import SocialLinkInput from 'pages/profile-page/components/SocialLinkInput' import { ProfileTopTags } from 'pages/profile-page/components/desktop/ProfileTopTags' +import { useSsrContext } from 'ssr/SsrContext' import { ProfileBio } from './ProfileBio' import { ProfileMutuals } from './ProfileMutuals' @@ -93,6 +95,7 @@ export const ProfileLeftNav = (props: ProfileLeftNavProps) => { } = props const accountUser = useSelector(getAccountUser) + const { isSsrEnabled } = useSsrContext() const renderTipAudioButton = (_: any, style: object) => ( @@ -179,40 +182,42 @@ export const ProfileLeftNav = (props: ProfileLeftNavProps) => {
    ) - } else if (!loading && !isDeactivated) { + } else if ((!loading || isSsrEnabled) && !isDeactivated) { const showUploadChip = isOwner && !isArtist return (
    - - {!accountUser || accountUser.user_id !== userId ? ( - - ) : null} - {allowAiAttribution ? ( -
    - -
    - ) : null} - -
    - - - - {isArtist ? : null} - {showUploadChip ? ( - + + + {!accountUser || accountUser.user_id !== userId ? ( + ) : null} -
    + {allowAiAttribution ? ( +
    + +
    + ) : null} + +
    + + + + {isArtist ? : null} + {showUploadChip ? ( + + ) : null} +
    +
    ) } else { diff --git a/packages/web/src/pages/profile-page/components/desktop/ProfilePage.module.css b/packages/web/src/pages/profile-page/components/desktop/ProfilePage.module.css index 2fe1e2229a2..75a57d5041f 100644 --- a/packages/web/src/pages/profile-page/components/desktop/ProfilePage.module.css +++ b/packages/web/src/pages/profile-page/components/desktop/ProfilePage.module.css @@ -4,11 +4,6 @@ min-width: 788px; } -.headerWrapper { - position: relative; - width: 100%; -} - /* Style for static profile wrapper content */ .mask { @@ -66,7 +61,8 @@ .nameWrapper .editableName { max-width: 440px; } -.nameWrapper .name { +.nameWrapper .name, +.nameWrapper .editableName { margin-top: -8px; } @@ -202,12 +198,6 @@ } /* Style for content section */ - -.inset { - display: flex; - flex-direction: column; -} - .content { max-width: 753px; padding-left: 229px; @@ -248,30 +238,6 @@ margin-bottom: 16px !important; } -.truncateContainer { - width: fit-content; - display: flex; - align-items: center; - font-weight: var(--harmony-font-bold); - font-size: var(--harmony-font-s); - color: var(--harmony-n-400); - margin-top: 16px; -} -.truncateContainer:hover { - cursor: pointer; -} -.truncateContainer svg { - margin-left: 8px; -} -.truncateContainer:hover span { - color: var(--harmony-secondary); - transition: color 0.07s ease-out; -} -.truncateContainer:hover svg path { - fill: var(--harmony-secondary); - transition: fill 0.07s ease-out; -} - .socialsTruncated > * { margin-right: 32px; margin-bottom: 0px !important; diff --git a/packages/web/src/pages/profile-page/components/desktop/ProfilePage.tsx b/packages/web/src/pages/profile-page/components/desktop/ProfilePage.tsx index 8adf8ea9189..b6ef6b7942c 100644 --- a/packages/web/src/pages/profile-page/components/desktop/ProfilePage.tsx +++ b/packages/web/src/pages/profile-page/components/desktop/ProfilePage.tsx @@ -20,6 +20,8 @@ import { ProfileUser } from '@audius/common/store' import { + Box, + Flex, IconAlbum, IconCollectible as IconCollectibles, IconNote, @@ -28,6 +30,7 @@ import { } from '@audius/harmony' import Card from 'components/card/desktop/Card' +import { ClientOnly } from 'components/client-only/ClientOnly' import CollectiblesPage from 'components/collectibles/components/CollectiblesPage' import CoverPhoto from 'components/cover-photo/CoverPhoto' import CardLineup from 'components/lineup/CardLineup' @@ -684,12 +687,14 @@ const ProfilePage = ({ elements, pathname: profilePage(handle) }) + const { title = '', description = '', canonicalUrl = '', structuredData } = getUserPageSEOFields({ handle, userName: name, bio }) + return ( -
    + -
    + -
    - {profile && profile.is_deactivated ? ( - - ) : ( - body - )} -
    -
    + +
    + {profile && profile.is_deactivated ? ( + + ) : ( + body + )} +
    +
    + -
    - {profile ? ( - <> - - - - ) : null} +
    + {/* NOTE: KJ - This ClientOnly might not be necessary, but helps keep server computation down */} + + {profile ? ( + <> + + + + ) : null} + ) } diff --git a/packages/web/src/pages/profile-page/components/desktop/ProfileWrapping.tsx b/packages/web/src/pages/profile-page/components/desktop/ProfileWrapping.tsx index 7375249d07c..2262ffc476b 100644 --- a/packages/web/src/pages/profile-page/components/desktop/ProfileWrapping.tsx +++ b/packages/web/src/pages/profile-page/components/desktop/ProfileWrapping.tsx @@ -5,7 +5,7 @@ import cn from 'classnames' import ProfilePicture from 'components/profile-picture/ProfilePicture' import FollowsYouBadge from 'components/user-badges/FollowsYouBadge' -import EditableName from 'pages/profile-page/components/EditableName' +import { EditableName } from 'pages/profile-page/components/EditableName' import { ProfileLeftNav } from './ProfileLeftNav' import styles from './ProfilePage.module.css' diff --git a/packages/web/src/pages/profile-page/components/mobile/GrowingCoverPhoto.tsx b/packages/web/src/pages/profile-page/components/mobile/GrowingCoverPhoto.tsx index e05cfe9d0a2..89b8b15f358 100644 --- a/packages/web/src/pages/profile-page/components/mobile/GrowingCoverPhoto.tsx +++ b/packages/web/src/pages/profile-page/components/mobile/GrowingCoverPhoto.tsx @@ -1,12 +1,18 @@ import { useEffect, useCallback } from 'react' import { useInstanceVar } from '@audius/common/hooks' +import { WidthSizes } from '@audius/common/models' // eslint-disable-next-line no-restricted-imports -- TODO: migrate to @react-spring/web import { useSpring, animated } from 'react-spring' import DynamicImage, { DynamicImageProps } from 'components/dynamic-image/DynamicImage' +import { + StaticImage, + StaticImageProps +} from 'components/static-image/StaticImage' +import { useSsrContext } from 'ssr/SsrContext' // Scale the image by making it larger relative to y-pos and translate it up slightly (capping at -15px) so it // covers the gap made by overscroll. @@ -33,12 +39,10 @@ const springConfig = { * Same props as DynamicImage. */ const GrowingCoverPhoto = ({ - image, - imageStyle, - wrapperClassName, children, ...rest -}: DynamicImageProps) => { +}: DynamicImageProps | StaticImageProps) => { + const { isSsrEnabled } = useSsrContext() const [getShouldTrackScroll, setShouldTrackScroll] = useInstanceVar(false) const [springProps, setSpringProps] = useSpring(() => ({ to: { @@ -105,6 +109,8 @@ const GrowingCoverPhoto = ({ } }, [handleScrollEvent, handleReset, handleTouch]) + const ImageElement = isSsrEnabled ? StaticImage : DynamicImage + return ( - + {children} - + ) } diff --git a/packages/web/src/pages/profile-page/components/mobile/ProfileHeader.module.css b/packages/web/src/pages/profile-page/components/mobile/ProfileHeader.module.css index 12737b69a96..3f1693ae703 100644 --- a/packages/web/src/pages/profile-page/components/mobile/ProfileHeader.module.css +++ b/packages/web/src/pages/profile-page/components/mobile/ProfileHeader.module.css @@ -20,7 +20,7 @@ } .profilePictureWrapper { - position: absolute; + position: absolute !important; top: 37px; left: 11px; height: 82px; @@ -70,16 +70,6 @@ flex-direction: column; } -.titleContainer .right { - display: flex; - flex: 1; - justify-content: flex-end; -} - -.subscribeButton { - margin-right: 6px; -} - .artistName { color: var(--harmony-neutral); font-family: var(--harmony-font-family); diff --git a/packages/web/src/pages/profile-page/components/mobile/ProfileHeader.tsx b/packages/web/src/pages/profile-page/components/mobile/ProfileHeader.tsx index ad6cb94c47b..4b3f0996f1b 100644 --- a/packages/web/src/pages/profile-page/components/mobile/ProfileHeader.tsx +++ b/packages/web/src/pages/profile-page/components/mobile/ProfileHeader.tsx @@ -12,6 +12,7 @@ import { ProfilePictureSizes, CoverPhotoSizes } from '@audius/common/models' +import { ProfileUser } from '@audius/common/store' import { formatCount } from '@audius/common/utils' import { IconArtistBadge as BadgeArtist, @@ -29,8 +30,10 @@ import cn from 'classnames' import { make, useRecord } from 'common/store/analytics/actions' import { ArtistRecommendationsDropdown } from 'components/artist-recommendations/ArtistRecommendationsDropdown' +import { ClientOnly } from 'components/client-only/ClientOnly' import DynamicImage from 'components/dynamic-image/DynamicImage' import Skeleton from 'components/skeleton/Skeleton' +import { StaticImage } from 'components/static-image/StaticImage' import SubscribeButton from 'components/subscribe-button/SubscribeButton' import FollowsYouBadge from 'components/user-badges/FollowsYouBadge' import ProfilePageBadge from 'components/user-badges/ProfilePageBadge' @@ -38,6 +41,7 @@ import UserBadges from 'components/user-badges/UserBadges' import { UserGeneratedText } from 'components/user-generated-text' import { useCoverPhoto } from 'hooks/useCoverPhoto' import { useUserProfilePicture } from 'hooks/useUserProfilePicture' +import { useSsrContext } from 'ssr/SsrContext' import { FOLLOWING_USERS_ROUTE, FOLLOWERS_USERS_ROUTE } from 'utils/route' import GrowingCoverPhoto from './GrowingCoverPhoto' @@ -81,6 +85,7 @@ const LoadingProfileHeader = () => { type ProfileHeaderProps = { isDeactivated: boolean + profile: ProfileUser name: string handle: string isArtist: boolean @@ -131,6 +136,7 @@ function isEllipsisActive(e: HTMLElement) { const ProfileHeader = ({ isDeactivated, + profile, name, handle, isArtist, @@ -167,6 +173,7 @@ const ProfileHeader = ({ }: ProfileHeaderProps) => { const [hasEllipsis, setHasEllipsis] = useState(false) const [isDescriptionMinimized, setIsDescriptionMinimized] = useState(true) + const { isSsrEnabled } = useSsrContext() const bioRef = useRef(null) const isEditing = mode === 'editing' @@ -198,10 +205,7 @@ const ProfileHeader = ({ } }, [website, donation, hasEllipsis, setHasEllipsis]) - let { source: coverPhoto, shouldBlur } = useCoverPhoto( - userId, - WidthSizes.SIZE_2000 - ) + let { source: coverPhoto } = useCoverPhoto(userId, WidthSizes.SIZE_2000) coverPhoto = isDeactivated ? imageProfilePicEmpty : coverPhoto let coverPhotoStyle = {} if (coverPhoto === imageCoverPhotoBlank) { @@ -288,34 +292,58 @@ const ProfileHeader = ({ // If we're not loading, we know that // nullable fields such as userId are valid. - if (loading) { + if (loading && !isSsrEnabled) { return } + const ImageElement = isSsrEnabled ? StaticImage : DynamicImage + + const isUserMissingImage = + !profile?.cover_photo_sizes && !profile?.profile_picture_sizes + return (
    {isArtist && !isEditing && !isDeactivated ? ( ) : null} {isEditing && } - {isEditing && } - + {!isEditing && !isDeactivated && (
    @@ -337,33 +365,34 @@ const ProfileHeader = ({
    -
    - {following && ( - - )} - {mode === 'owner' ? ( - - ) : ( - onFollow(userId)} - onUnfollow={() => onUnfollow(userId)} - fullWidth={false} - /> - )} -
    + + + {following ? ( + + ) : null} + {mode === 'owner' ? ( + + ) : ( + onFollow(userId)} + onUnfollow={() => onUnfollow(userId)} + fullWidth={false} + /> + )} + +
    @@ -466,14 +495,16 @@ const ProfileHeader = ({ {isDescriptionMinimized ? messages.showMore : messages.showLess}
    ) : null} - ( -

    Here are some accounts that vibe well with {name}

    - )} - artistId={userId} - onClose={onCloseArtistRecommendations} - /> + + ( +

    Here are some accounts that vibe well with {name}

    + )} + artistId={userId} + onClose={onCloseArtistRecommendations} + /> +
    )} {mode === 'owner' && !isEditing && } diff --git a/packages/web/src/pages/profile-page/components/mobile/ProfilePage.tsx b/packages/web/src/pages/profile-page/components/mobile/ProfilePage.tsx index ff24857bf2e..c06f5937cc5 100644 --- a/packages/web/src/pages/profile-page/components/mobile/ProfilePage.tsx +++ b/packages/web/src/pages/profile-page/components/mobile/ProfilePage.tsx @@ -29,6 +29,7 @@ import { import cn from 'classnames' import Card from 'components/card/mobile/Card' +import { ClientOnly } from 'components/client-only/ClientOnly' import CollectiblesPage from 'components/collectibles/components/CollectiblesPage' import { HeaderContext } from 'components/header/mobile/HeaderContextProvider' import CardLineup from 'components/lineup/CardLineup' @@ -42,6 +43,7 @@ import NavContext, { import TierExplainerDrawer from 'components/user-badges/TierExplainerDrawer' import useTabs, { TabHeader } from 'hooks/useTabs/useTabs' import { MIN_COLLECTIBLES_TIER } from 'pages/profile-page/ProfilePageProvider' +import { useSsrContext } from 'ssr/SsrContext' import { collectionPage, profilePage } from 'utils/route' import { getUserPageSEOFields } from 'utils/seo' import { withNullGuard } from 'utils/withNullGuard' @@ -300,6 +302,7 @@ const ProfilePage = g( onCloseArtistRecommendations }) => { const { setHeader } = useContext(HeaderContext) + const { isSsrEnabled } = useSsrContext() useEffect(() => { setHeader(null) }, [setHeader]) @@ -308,7 +311,7 @@ const ProfilePage = g( let content let profileTabs let profileElements - const isLoading = status === Status.LOADING + const isLoading = status === Status.LOADING && !isSsrEnabled const isEditing = mode === 'editing' // Set Nav-Bar Menu @@ -643,7 +646,7 @@ const ProfilePage = g( content = (
    {tabs}
    - {body} + {body}
    ) } @@ -666,6 +669,7 @@ const ProfilePage = g( > - {content} + {content} - + + + ) } diff --git a/packages/web/src/pages/settings-page/components/desktop/DeveloperApps/DeveloperAppsSettingsCard.tsx b/packages/web/src/pages/settings-page/components/desktop/DeveloperApps/DeveloperAppsSettingsCard.tsx index 86e59dba9e3..99d0b343416 100644 --- a/packages/web/src/pages/settings-page/components/desktop/DeveloperApps/DeveloperAppsSettingsCard.tsx +++ b/packages/web/src/pages/settings-page/components/desktop/DeveloperApps/DeveloperAppsSettingsCard.tsx @@ -30,7 +30,7 @@ export const DeveloperAppsSettingsCard = () => { title={messages.title} description={messages.description} > - diff --git a/packages/web/src/pages/settings-page/components/desktop/SettingsPage.tsx b/packages/web/src/pages/settings-page/components/desktop/SettingsPage.tsx index 4cdc9dbcd2b..11b5ece4bd3 100644 --- a/packages/web/src/pages/settings-page/components/desktop/SettingsPage.tsx +++ b/packages/web/src/pages/settings-page/components/desktop/SettingsPage.tsx @@ -351,7 +351,11 @@ export const SettingsPage = (props: SettingsPageProps) => { title={messages.inboxSettingsCardTitle} description={messages.inboxSettingsCardDescription} > - @@ -361,7 +365,11 @@ export const SettingsPage = (props: SettingsPageProps) => { title={messages.notificationsCardTitle} description={messages.notificationsCardDescription} > - @@ -377,7 +385,7 @@ export const SettingsPage = (props: SettingsPageProps) => { placement={ComponentPlacement.BOTTOM} fillParent={false} > - @@ -387,7 +395,7 @@ export const SettingsPage = (props: SettingsPageProps) => { title={messages.changeEmailCardTitle} description={messages.changeEmailCardDescription} > - @@ -396,7 +404,11 @@ export const SettingsPage = (props: SettingsPageProps) => { title={messages.changePasswordCardTitle} description={messages.changePasswordCardDescription} > - @@ -413,7 +425,7 @@ export const SettingsPage = (props: SettingsPageProps) => { ) : null} ) : ( - )} diff --git a/packages/web/src/pages/sign-in-page/SignInPage.tsx b/packages/web/src/pages/sign-in-page/SignInPage.tsx index 93ceb77ff09..5d0fe8a3621 100644 --- a/packages/web/src/pages/sign-in-page/SignInPage.tsx +++ b/packages/web/src/pages/sign-in-page/SignInPage.tsx @@ -13,6 +13,7 @@ import { import { Form, Formik, useField } from 'formik' import { useDispatch } from 'react-redux' import { Link } from 'react-router-dom' +import { useWindowSize } from 'react-use' import { toFormikValidationSchema } from 'zod-formik-adapter' import audiusLogoColored from 'assets/img/audiusLogoColored.png' @@ -37,6 +38,8 @@ import { SignInWithMetaMaskButton } from './SignInWithMetaMaskButton' const SignInSchema = toFormikValidationSchema(signInSchema) +const smallDesktopWindowHeight = 900 + type SignInValues = { email: string password: string @@ -45,6 +48,8 @@ type SignInValues = { export const SignInPage = () => { const dispatch = useDispatch() const { isMobile } = useMedia() + const { height: windowHeight } = useWindowSize() + const isSmallDesktop = windowHeight < smallDesktopWindowHeight const navigate = useNavigateToPage() const [showForgotPassword, setShowForgotPassword] = useState(false) const { value: existingEmail } = useSelector(getEmailField) @@ -94,8 +99,8 @@ export const SignInPage = () => { gap='l' > - - {isMobile ? ( + + {isMobile || isSmallDesktop ? ( ) : ( { {!isMobile ? ( - ) : null} diff --git a/packages/web/src/pages/sign-on-page/AudiusValues.tsx b/packages/web/src/pages/sign-on-page/AudiusValues.tsx index b6362b40c3d..3ee82d9f456 100644 --- a/packages/web/src/pages/sign-on-page/AudiusValues.tsx +++ b/packages/web/src/pages/sign-on-page/AudiusValues.tsx @@ -22,17 +22,24 @@ type AudiusValueProps = { icon: IconComponent; text: string; dynamic?: boolean } */ const AudiusValue = (props: AudiusValueProps) => { const { icon: Icon, text } = props + const isMedium = useMediaQuery( + '(max-width: 1363px) and (min-width: 1094px), (max-width: 860px) and (min-width: 645px)' + ) const isSmall = useMediaQuery( - '(max-width: 1363px) and (min-width: 860px), (max-width: 645px)' + '(max-width: 1094px) and (min-width: 860px), (max-width: 645px)' ) return ( - + { const isMobile = useMediaQuery('(max-width: 860px)') + + const isMedium = useMediaQuery( + '(max-width: 1363px) and (min-width: 1094px), (max-width: 860px) and (min-width: 645px)' + ) const isSmall = useMediaQuery( - '(max-width: 1363px) and (min-width: 860px), (max-width: 645px)' + '(max-width: 1094px) and (min-width: 860px), (max-width: 645px)' ) return ( {isMobile ? null : ( { const { children } = props - const { spacing, motion } = useTheme() + const { spacing, motion, color } = useTheme() const hideCloseButton = useRouteMatch({ path: [ @@ -77,7 +78,14 @@ const DesktopSignOnRoot = (props: RootProps) => { const isExpanded = !collapsedDesktopPageMatch return ( - + {!hideCloseButton ? ( { ) : null} - { export const SignOnPage = () => { const { isMobile } = useMedia() + const location = useLocation() + const dispatch = useDispatch() + + useEffectOnce(() => { + // Check for referrals and set them in the store + const referrerHandle = new URLSearchParams(location.search).get('ref') + if (referrerHandle) { + dispatch(fetchReferrer(referrerHandle)) + } + }) const [isLoaded, setIsLoaded] = useState(false) const SignOnRoot = isMobile ? MobileSignOnRoot : DesktopSignOnRoot diff --git a/packages/web/src/pages/sign-up-page/pages/CreateEmailPage.tsx b/packages/web/src/pages/sign-up-page/pages/CreateEmailPage.tsx index 68a22925353..3783b682f07 100644 --- a/packages/web/src/pages/sign-up-page/pages/CreateEmailPage.tsx +++ b/packages/web/src/pages/sign-up-page/pages/CreateEmailPage.tsx @@ -16,6 +16,7 @@ import { import { Form, Formik } from 'formik' import { useDispatch, useSelector } from 'react-redux' import { Link } from 'react-router-dom' +import { useWindowSize } from 'react-use' import { toFormikValidationSchema } from 'zod-formik-adapter' import audiusLogoColored from 'assets/img/audiusLogoColored.png' @@ -46,12 +47,16 @@ import { SocialMediaLoading } from '../components/SocialMediaLoading' import { Heading, Page } from '../components/layout' import { useSocialMediaLoader } from '../hooks/useSocialMediaLoader' +const smallDesktopWindowHeight = 900 + export type SignUpEmailValues = { email: string } export const CreateEmailPage = () => { const { isMobile } = useMedia() + const { height: windowHeight } = useWindowSize() + const isSmallDesktop = windowHeight < smallDesktopWindowHeight const dispatch = useDispatch() const navigate = useNavigateToPage() const existingEmailValue = useSelector(getEmailField) @@ -117,8 +122,8 @@ export const CreateEmailPage = () => { > {({ isSubmitting }) => ( - - {isMobile ? ( + + {isMobile || isSmallDesktop ? ( ) : ( { - {createEmailPageMessages.subHeader.line1} -
    {createEmailPageMessages.subHeader.line2} - - } tag='h1' centered={isMobile} /> diff --git a/packages/web/src/pages/track-page/components/desktop/TrackPage.tsx b/packages/web/src/pages/track-page/components/desktop/TrackPage.tsx index 818fa690591..3dbf7da9f6a 100644 --- a/packages/web/src/pages/track-page/components/desktop/TrackPage.tsx +++ b/packages/web/src/pages/track-page/components/desktop/TrackPage.tsx @@ -56,15 +56,6 @@ export type OwnProps = { onSaveTrack: (isSaved: boolean, trackId: ID) => void makePublic: (trackId: ID) => void - onDownloadTrack: ({ - trackId, - category, - parentTrackId - }: { - trackId: ID - category?: string - parentTrackId?: ID - }) => void // Tracks Lineup Props tracks: LineupState currentQueueItem: QueueItem @@ -95,7 +86,6 @@ const TrackPage = ({ onSaveTrack, onFollow, onUnfollow, - onDownloadTrack, makePublic, onClickReposts, onClickFavorites, @@ -114,7 +104,7 @@ const TrackPage = ({ const isSaved = heroTrack?.has_current_user_saved ?? false const isReposted = heroTrack?.has_current_user_reposted ?? false - const { isFetchingNFTAccess, hasStreamAccess, hasDownloadAccess } = + const { isFetchingNFTAccess, hasStreamAccess } = useGatedContentAccess(heroTrack) const loading = !heroTrack || isFetchingNFTAccess @@ -169,7 +159,6 @@ const TrackPage = ({ isDownloadGated={defaults.isDownloadGated} downloadConditions={defaults.downloadConditions} hasStreamAccess={hasStreamAccess} - hasDownloadAccess={hasDownloadAccess} isRemix={!!defaults.remixParentTrackId} isPublishing={defaults.isPublishing} fieldVisibility={defaults.fieldVisibility} @@ -183,7 +172,6 @@ const TrackPage = ({ following={following} onFollow={onFollow} onUnfollow={onUnfollow} - onDownload={onDownloadTrack} onMakePublic={makePublic} onClickReposts={onClickReposts} onClickFavorites={onClickFavorites} diff --git a/packages/web/src/pages/upload-page/components/ShareBanner.tsx b/packages/web/src/pages/upload-page/components/ShareBanner.tsx index 42d282f0346..a77d73ca704 100644 --- a/packages/web/src/pages/upload-page/components/ShareBanner.tsx +++ b/packages/web/src/pages/upload-page/components/ShareBanner.tsx @@ -195,7 +195,7 @@ export const ShareBanner = (props: ShareBannerProps) => {
    ) : null}
    -
    +
    +
    { const [{ value: tracks }] = useField('tracks') @@ -21,7 +25,12 @@ export const CollectionTrackFieldArray = () => { > {(provided) => ( -
    +
    {tracks.map((track, index) => ( { ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} + role='listitem' > {stems.map((stem, i) => ( 1} diff --git a/packages/web/src/public-site/pages/landing-page/components/CTAGetStarted.tsx b/packages/web/src/public-site/pages/landing-page/components/CTAGetStarted.tsx index cd3c2266e92..5dee870a60b 100644 --- a/packages/web/src/public-site/pages/landing-page/components/CTAGetStarted.tsx +++ b/packages/web/src/public-site/pages/landing-page/components/CTAGetStarted.tsx @@ -100,7 +100,7 @@ const CTAGetStarted = (props: CTAGetStartedProps) => { className={styles.ctaButton} > {messages.cta} - +
    @@ -145,7 +145,7 @@ const CTAGetStarted = (props: CTAGetStartedProps) => { className={styles.ctaButton} > {messages.cta} - +
    diff --git a/packages/web/src/services/sentry.ts b/packages/web/src/services/sentry.ts index d55bd20c6ba..8fae41a790e 100644 --- a/packages/web/src/services/sentry.ts +++ b/packages/web/src/services/sentry.ts @@ -18,11 +18,7 @@ export const initializeSentry = () => { init({ dsn: env.SENTRY_DSN, ignoreErrors: - typeof navigator !== 'undefined' - ? navigator?.userAgent === 'probers' - ? [/.*/] - : undefined - : undefined, + process.env.VITE_SENTRY_DISABLED === 'true' ? [/.*/] : undefined, // Need to give Sentry a version so it can // associate stacktraces with sourcemaps diff --git a/packages/web/src/ssr/profile/+onBeforeRender.tsx b/packages/web/src/ssr/profile/+onBeforeRender.tsx index c43f8b38f39..724c20760aa 100644 --- a/packages/web/src/ssr/profile/+onBeforeRender.tsx +++ b/packages/web/src/ssr/profile/+onBeforeRender.tsx @@ -2,7 +2,7 @@ import { Maybe } from '@audius/common/utils' import { full as FullSdk } from '@audius/sdk' import type { PageContextServer } from 'vike/types' -import { audiusSdk } from 'ssr/util' +import { audiusSdk } from '../sdk' export type ProfilePageProps = { user: Maybe diff --git a/packages/web/src/ssr/profile/route.tsx b/packages/web/src/ssr/profile/+route.tsx similarity index 55% rename from packages/web/src/ssr/profile/route.tsx rename to packages/web/src/ssr/profile/+route.tsx index 6026ab22274..0997b37958e 100644 --- a/packages/web/src/ssr/profile/route.tsx +++ b/packages/web/src/ssr/profile/+route.tsx @@ -1,4 +1,3 @@ -// NOTE: This ssr route is off until the profile page is rendered server-side import { makePageRoute } from 'ssr/util' export default makePageRoute('/@handle', 'Profile Page') diff --git a/packages/web/src/ssr/sdk.ts b/packages/web/src/ssr/sdk.ts new file mode 100644 index 00000000000..c8bc6154d72 --- /dev/null +++ b/packages/web/src/ssr/sdk.ts @@ -0,0 +1,27 @@ +import { + sdk, + DiscoveryNodeSelector, + productionConfig, + stagingConfig, + developmentConfig +} from '@audius/sdk' + +const sdkConfigs = { + production: productionConfig, + staging: stagingConfig, + development: developmentConfig +} + +const discoveryNodeSelector = new DiscoveryNodeSelector({ + bootstrapServices: ( + sdkConfigs[process.env.VITE_ENVIRONMENT as keyof typeof sdkConfigs] ?? + productionConfig + ).discoveryNodes +}) + +export const audiusSdk = sdk({ + appName: process.env.VITE_PUBLIC_HOSTNAME ?? 'audius.co', + services: { + discoveryNodeSelector + } +}) diff --git a/packages/web/src/ssr/track/+onBeforeRender.tsx b/packages/web/src/ssr/track/+onBeforeRender.tsx index d9f64231dfe..72a5b25595b 100644 --- a/packages/web/src/ssr/track/+onBeforeRender.tsx +++ b/packages/web/src/ssr/track/+onBeforeRender.tsx @@ -2,7 +2,7 @@ import { Maybe } from '@audius/common/utils' import { full as FullSdk } from '@audius/sdk' import type { PageContextServer } from 'vike/types' -import { audiusSdk } from 'ssr/util' +import { audiusSdk } from '../sdk' export type TrackPageProps = { track: Maybe diff --git a/packages/web/src/ssr/util.ts b/packages/web/src/ssr/util.ts index 09b76afe677..8ff30d99039 100644 --- a/packages/web/src/ssr/util.ts +++ b/packages/web/src/ssr/util.ts @@ -1,35 +1,8 @@ -import { - sdk, - DiscoveryNodeSelector, - productionConfig, - stagingConfig, - developmentConfig -} from '@audius/sdk' import { resolveRoute } from 'vike/routing' import type { PageContextServer } from 'vike/types' import { staticRoutes } from 'utils/route' -const sdkConfigs = { - production: productionConfig, - staging: stagingConfig, - development: developmentConfig -} - -const discoveryNodeSelector = new DiscoveryNodeSelector({ - bootstrapServices: ( - sdkConfigs[process.env.VITE_ENVIRONMENT as keyof typeof sdkConfigs] ?? - productionConfig - ).discoveryNodes -}) - -export const audiusSdk = sdk({ - appName: process.env.VITE_PUBLIC_HOSTNAME ?? 'audius.co', - services: { - discoveryNodeSelector - } -}) - const assetPaths = new Set(['src', 'assets', 'scripts', 'fonts', 'favicons']) const invalidPaths = new Set(['undefined']) diff --git a/packages/web/src/utils/animations/background-waves.js b/packages/web/src/utils/animations/background-waves.js deleted file mode 100644 index a783d1811e1..00000000000 --- a/packages/web/src/utils/animations/background-waves.js +++ /dev/null @@ -1,166 +0,0 @@ -import * as PIXI from 'pixi.js' - -const bgLargeWave = 'src/assets/img/bgWaveLarge.svg' -const bgSmallWave = 'src/assets/img/bgWaveSmall.svg' - -// Prevent Pixi lib from printing to console -PIXI.utils.skipHello() - -export class Wave { - constructor(type, height, width, opacity, speed, yPos, offset = 0) { - const texture = - type === 'small' - ? PIXI.loader.resources[bgSmallWave].texture - : PIXI.loader.resources[bgLargeWave].texture - this.height = height - this.width = width - this.opacity = opacity - this.speed = speed - this.sprites = [new PIXI.Sprite(texture)] - this.spriteWidth = this.sprites[0].width - this.spriteHeight = this.sprites[0].height - - this.offset = - typeof offset === 'function' ? offset(width, this.spriteWidth) : offset - this.numImages = Math.ceil(width / this.spriteWidth) + 1 - for (let i = 0; i < this.numImages - 1; i++) { - this.sprites.push(new PIXI.Sprite(texture)) - } - this.start = -1 * this.spriteWidth + this.offset - this.sprites.forEach((sprite, i) => { - sprite.blendMode = PIXI.BLEND_MODES.SCREEN - sprite.alpha = this.opacity - sprite.y = yPos(height, this.spriteHeight) - sprite.x = this.start + i * this.spriteWidth - }) - } - - update = () => { - this.start = 0 - this.sprites.forEach((sprite) => { - sprite.x = sprite.x + this.speed * 0.16 - if (sprite.x < this.start) this.start = sprite.x - }) - this.sprites.forEach((sprite) => { - if (sprite.x > this.width) { - sprite.x = this.state - this.spriteWidth - this.start = sprite.x - } - }) - } -} - -const resourcesToLoad = [bgSmallWave, bgLargeWave] -const isReady = new Promise((resolve) => - PIXI.loader.add(resourcesToLoad).load(() => resolve(true)) -) - -export default class WaveBG { - static loadResources = async () => { - return isReady - } - - constructor(container, useStatic, waveConfig) { - this.container = container - this.app = new PIXI.Application({ transparent: true }) - this.app.renderer.view.style.position = 'absolute' - this.app.renderer.view.style.display = 'block' - this.app.renderer.autoResize = true - this.app.renderer.resize(window.innerWidth, window.innerHeight) - this.useStatic = useStatic - this.waveConfig = waveConfig - this.setup() - } - - getBackground = (width, height) => { - const radius = Math.sqrt(width ** 2 + height ** 2) - const bgCanvas = document.createElement('canvas') - bgCanvas.width = width - bgCanvas.height = height - const backgroundContext = bgCanvas.getContext('2d', { alpha: false }) - - const gradient = backgroundContext.createRadialGradient( - 0, - 0, - Math.ceil(radius / 2), - 0, - 0, - Math.ceil(radius) - ) - - // Add three color stops - gradient.addColorStop(0, '#B749D6') - gradient.addColorStop(1, '#6516A3') - - // Set the fill style and draw a rectangle - backgroundContext.fillStyle = gradient - backgroundContext.fillRect(0, 0, width, height) - return new PIXI.Sprite(PIXI.Texture.fromCanvas(bgCanvas)) - } - - setup = () => { - if (this.container) { - this.container.appendChild(this.app.view) - this.width = this.app.renderer.width - this.height = this.app.renderer.height - this.background = this.getBackground(this.width, this.height) - this.app.stage.addChild(this.background) - this.createWaves() - // Listen for frame updates - if (!this.useStatic) { - this.app.ticker.add(() => { - this.waves.forEach((wave) => { - wave.update() - }) - }) - } - } - } - - createWaves = () => { - this.waves = this.waveConfig.map((waveConfig) => { - return new Wave( - waveConfig.type, - this.app.renderer.height, - this.app.renderer.width, - waveConfig.opacity, - waveConfig.speed, - waveConfig.yPos, - waveConfig.offset - ) - }) - this.waves.forEach((wave) => - wave.sprites.forEach((sprite) => { - this.app.stage.addChild(sprite) - }) - ) - } - - resize = () => { - this.app.renderer.resize(window.innerWidth, window.innerHeight) - this.width = this.app.renderer.width - this.height = this.app.renderer.height - this.background = this.getBackground(this.width, this.height) - this.app.stage.addChild(this.background) - this.waves.forEach((wave) => - wave.sprites.forEach((sprite) => { - this.app.stage.removeChild(sprite) - }) - ) - this.createWaves() - } - - pause = () => { - this.app.stop() - } - - start = () => { - this.app.start() - } - - remove = () => { - this.app.stop() - this.app.destroy(true) - this.app = null - } -} diff --git a/packages/web/src/utils/clientUtil.ts b/packages/web/src/utils/clientUtil.ts index 7fe301126f7..7aee1673334 100644 --- a/packages/web/src/utils/clientUtil.ts +++ b/packages/web/src/utils/clientUtil.ts @@ -38,9 +38,6 @@ export const isMobile = () => { check = true } - if (navigator.userAgent === 'probers' && window.innerWidth < 500) { - check = true - } return check } diff --git a/packages/web/src/utils/route.ts b/packages/web/src/utils/route.ts index add585d58dc..0c7288c420d 100644 --- a/packages/web/src/utils/route.ts +++ b/packages/web/src/utils/route.ts @@ -302,6 +302,18 @@ export const staticRoutes = new Set([ EMPTY_PAGE, SIGN_IN_PAGE, SIGN_UP_PAGE, + ...SIGN_ON_ALIASES, + SIGN_UP_EMAIL_PAGE, + SIGN_UP_PASSWORD_PAGE, + SIGN_UP_CREATE_LOGIN_DETAILS, + SIGN_UP_HANDLE_PAGE, + SIGN_UP_REVIEW_HANDLE_PAGE, + SIGN_UP_FINISH_PROFILE_PAGE, + SIGN_UP_GENRES_PAGE, + SIGN_UP_ARTISTS_PAGE, + SIGN_UP_APP_CTA_PAGE, + SIGN_UP_LOADING_PAGE, + SIGN_UP_COMPLETED_REDIRECT, NOTIFICATION_PAGE, APP_REDIRECT, REPOSTING_USERS_ROUTE, diff --git a/protocol-dashboard/package.json b/protocol-dashboard/package.json index 4f27565dc21..3b476fb34e8 100644 --- a/protocol-dashboard/package.json +++ b/protocol-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "audius-protocol-dashboard", - "version": "0.1.9", + "version": "0.1.10", "private": true, "type": "module", "homepage": "./",