diff --git a/packages/common/src/models/Collection.ts b/packages/common/src/models/Collection.ts index d7bad2a5c84..765613ced4f 100644 --- a/packages/common/src/models/Collection.ts +++ b/packages/common/src/models/Collection.ts @@ -58,6 +58,7 @@ export type CollectionMetadata = { offline?: OfflineCollectionMetadata local?: boolean release_date?: string + ddex_app?: string | null } export type CollectionDownloadReason = { is_from_favorites: boolean } diff --git a/packages/common/src/models/Track.ts b/packages/common/src/models/Track.ts index 64e945ef2c0..645d182f8c6 100644 --- a/packages/common/src/models/Track.ts +++ b/packages/common/src/models/Track.ts @@ -220,6 +220,7 @@ export type TrackMetadata = { orig_filename: Nullable is_downloadable: boolean is_original_available: boolean + ddex_app?: Nullable // Optional Fields is_playlist_upload?: boolean diff --git a/packages/common/src/services/audius-api-client/types.ts b/packages/common/src/services/audius-api-client/types.ts index cf5d8101ad2..51d4dbe5fcb 100644 --- a/packages/common/src/services/audius-api-client/types.ts +++ b/packages/common/src/services/audius-api-client/types.ts @@ -153,6 +153,7 @@ export type APITrack = { orig_filename: Nullable is_downloadable: boolean is_original_available: boolean + ddex_app: Nullable } export type APISearchTrack = Omit< diff --git a/packages/discovery-provider/ddl/migrations/0058_add_ddex_app_field.sql b/packages/discovery-provider/ddl/migrations/0058_add_ddex_app_field.sql new file mode 100644 index 00000000000..5c72669d957 --- /dev/null +++ b/packages/discovery-provider/ddl/migrations/0058_add_ddex_app_field.sql @@ -0,0 +1,6 @@ +begin; + +alter table tracks add column if not exists ddex_app varchar; +alter table playlists add column if not exists ddex_app varchar; + +commit; diff --git a/packages/discovery-provider/integration_tests/tasks/entity_manager/test_utils.py b/packages/discovery-provider/integration_tests/tasks/entity_manager/test_utils.py index 8fc84ebc8ca..7fc25ee2566 100644 --- a/packages/discovery-provider/integration_tests/tasks/entity_manager/test_utils.py +++ b/packages/discovery-provider/integration_tests/tasks/entity_manager/test_utils.py @@ -188,6 +188,7 @@ def test_valid_parse_metadata(app): "preview_start_seconds": None, "audio_upload_id": None, "placement_hosts": None, + "ddex_app": None, }, "QmUpdatePlaylist1": { "playlist_id": 1, @@ -198,6 +199,7 @@ def test_valid_parse_metadata(app): "is_album": False, "is_private": False, "is_image_autogenerated": None, + "ddex_app": None, }, } diff --git a/packages/discovery-provider/plugins/notifications/src/types/dn.ts b/packages/discovery-provider/plugins/notifications/src/types/dn.ts index 03d6948891e..2f0067d0abe 100644 --- a/packages/discovery-provider/plugins/notifications/src/types/dn.ts +++ b/packages/discovery-provider/plugins/notifications/src/types/dn.ts @@ -380,6 +380,7 @@ export interface PlaylistRow { txhash?: string upc?: string | null updated_at: Date + ddex_app?: string | null } export interface PlayRow { city?: string | null @@ -639,6 +640,7 @@ export interface TrackRow { track_segments: any txhash?: string updated_at: Date + ddex_app?: string | null } export interface TrendingParamRow { genre?: string | null diff --git a/packages/discovery-provider/plugins/pedalboard/packages/storage/src/index.ts b/packages/discovery-provider/plugins/pedalboard/packages/storage/src/index.ts index 4a42f237545..3cceb8ab0f9 100644 --- a/packages/discovery-provider/plugins/pedalboard/packages/storage/src/index.ts +++ b/packages/discovery-provider/plugins/pedalboard/packages/storage/src/index.ts @@ -709,6 +709,7 @@ export type Playlists = { slot: number | null; metadata_multihash: string | null; is_image_autogenerated: boolean; + ddex_app: string | null; }; export type Plays = { @@ -988,6 +989,7 @@ export type Tracks = { is_original_available: boolean; orig_file_cid: string | null; orig_filename: string | null; + ddex_app: string | null; }; export type TrendingResults = { diff --git a/packages/discovery-provider/src/api/v1/models/playlists.py b/packages/discovery-provider/src/api/v1/models/playlists.py index 51a898c618b..2386888bd0e 100644 --- a/packages/discovery-provider/src/api/v1/models/playlists.py +++ b/packages/discovery-provider/src/api/v1/models/playlists.py @@ -39,6 +39,7 @@ "favorite_count": fields.Integer(required=True), "total_play_count": fields.Integer(required=True), "user": fields.Nested(user_model, required=True), + "ddex_app": fields.String(allow_null=True), }, ) diff --git a/packages/discovery-provider/src/api/v1/models/tracks.py b/packages/discovery-provider/src/api/v1/models/tracks.py index eb596bc4183..8a9a15f6dbd 100644 --- a/packages/discovery-provider/src/api/v1/models/tracks.py +++ b/packages/discovery-provider/src/api/v1/models/tracks.py @@ -116,6 +116,7 @@ "play_count": fields.Integer(required=True), "permalink": fields.String, "is_streamable": fields.Boolean, + "ddex_app": fields.String(allow_null=True), }, ) diff --git a/packages/discovery-provider/src/models/playlists/playlist.py b/packages/discovery-provider/src/models/playlists/playlist.py index 3ee90ea7937..cbaaf067f82 100644 --- a/packages/discovery-provider/src/models/playlists/playlist.py +++ b/packages/discovery-provider/src/models/playlists/playlist.py @@ -41,6 +41,7 @@ class Playlist(Base, RepresentableMixin): upc = Column(String) updated_at = Column(DateTime, nullable=False) playlist_image_sizes_multihash = Column(String) + ddex_app = Column(String) is_image_autogenerated = Column( Boolean, nullable=False, server_default=text("false") ) diff --git a/packages/discovery-provider/src/models/tracks/track.py b/packages/discovery-provider/src/models/tracks/track.py index d90be73f24a..08151d0e67b 100644 --- a/packages/discovery-provider/src/models/tracks/track.py +++ b/packages/discovery-provider/src/models/tracks/track.py @@ -70,6 +70,7 @@ class Track(Base, RepresentableMixin): download = Column(JSONB()) is_scheduled_release = Column(Boolean, nullable=False, server_default=text("false")) is_unlisted = Column(Boolean, nullable=False, server_default=text("false")) + ddex_app = Column(String) field_visibility = Column(JSONB(True)) route_id = Column(String) stem_of = Column(JSONB(True)) diff --git a/packages/discovery-provider/src/schemas/playlist_schema.json b/packages/discovery-provider/src/schemas/playlist_schema.json index 1993858c919..6e08a06a86e 100644 --- a/packages/discovery-provider/src/schemas/playlist_schema.json +++ b/packages/discovery-provider/src/schemas/playlist_schema.json @@ -17,6 +17,10 @@ "is_image_autogenerated": { "type": "boolean", "default": false + }, + "ddex_app": { + "type": ["string", "null"], + "default": null } }, "required": [], diff --git a/packages/discovery-provider/src/schemas/track_schema.json b/packages/discovery-provider/src/schemas/track_schema.json index 2f8d32fdda7..755083789d4 100644 --- a/packages/discovery-provider/src/schemas/track_schema.json +++ b/packages/discovery-provider/src/schemas/track_schema.json @@ -158,6 +158,13 @@ ], "default": null }, + "ddex_app": { + "type": [ + "string", + "null" + ], + "default": null + }, "track_segments": { "type": "array", "minItems": 0, @@ -602,4 +609,4 @@ ] } } -} \ No newline at end of file +} 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 af882c4bfeb..da9438b1891 100644 --- a/packages/discovery-provider/src/tasks/entity_manager/entities/playlist.py +++ b/packages/discovery-provider/src/tasks/entity_manager/entities/playlist.py @@ -16,6 +16,7 @@ EntityType, ManageEntityParameters, copy_record, + is_ddex_signer, validate_signer, ) from src.tasks.metadata import immutable_playlist_fields @@ -344,6 +345,11 @@ def create_playlist(params: ManageEntityParameters): tracks = params.metadata["playlist_contents"].get("track_ids", []) tracks_with_index_time = [] last_added_to = None + + ddex_app = None + if is_ddex_signer(params.signer): + ddex_app = params.signer + for track in tracks: if "track" not in track or "time" not in track: raise IndexingValidationError( @@ -382,6 +388,7 @@ def create_playlist(params: ManageEntityParameters): last_added_to=last_added_to, is_current=False, is_delete=False, + ddex_app=ddex_app, ) update_playlist_routes_table(params, playlist_record, True) @@ -405,6 +412,16 @@ def dispatch_challenge_playlist_upload( ) +def validate_update_ddex_playlist(params: ManageEntityParameters, playlist_record): + if playlist_record.ddex_app: + if playlist_record.ddex_app != params.signer or not is_ddex_signer( + params.signer + ): + raise IndexingValidationError( + f"Signer {params.signer} does not have permission to {params.action} DDEX playlist {playlist_record.playlist_id}" + ) + + def update_playlist(params: ManageEntityParameters): validate_playlist_tx(params) # TODO ignore updates on deleted playlists? @@ -416,6 +433,8 @@ def update_playlist(params: ManageEntityParameters): ): # override with last updated playlist is in this block existing_playlist = params.new_records["Playlist"][playlist_id][-1] + validate_update_ddex_playlist(params, existing_playlist) + playlist_record = copy_record( existing_playlist, params.block_number, @@ -445,6 +464,8 @@ def delete_playlist(params: ManageEntityParameters): # override with last updated playlist is in this block existing_playlist = params.new_records["Playlist"][params.entity_id][-1] + validate_update_ddex_playlist(params, existing_playlist) + deleted_playlist = copy_record( existing_playlist, params.block_number, 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 473fba973af..cb4e06ac044 100644 --- a/packages/discovery-provider/src/tasks/entity_manager/entities/track.py +++ b/packages/discovery-provider/src/tasks/entity_manager/entities/track.py @@ -27,6 +27,7 @@ EntityType, ManageEntityParameters, copy_record, + is_ddex_signer, validate_signer, ) from src.tasks.metadata import immutable_track_fields @@ -463,6 +464,9 @@ def create_track(params: ManageEntityParameters): is_delete=False, ) + if is_ddex_signer(params.signer): + track_record.ddex_app = params.signer + update_track_routes_table( params, track_record, params.metadata, params.pending_track_routes ) @@ -484,6 +488,14 @@ def create_track(params: ManageEntityParameters): params.add_record(track_id, track_record) +def validate_update_ddex_track(params: ManageEntityParameters, track_record): + if track_record.ddex_app: + if track_record.ddex_app != params.signer or not is_ddex_signer(params.signer): + raise IndexingValidationError( + f"Signer {params.signer} does not have permission to {params.action} DDEX track {track_record.track_id}" + ) + + def update_track(params: ManageEntityParameters): handle = get_handle(params) validate_track_tx(params) @@ -495,6 +507,8 @@ def update_track(params: ManageEntityParameters): ): # override with last updated track is in this block existing_track = params.new_records["Track"][track_id][-1] + validate_update_ddex_track(params, existing_track) + track_record = copy_record( existing_track, params.block_number, @@ -528,6 +542,8 @@ def delete_track(params: ManageEntityParameters): # override with last updated playlist is in this block existing_track = params.new_records["Track"][params.entity_id][-1] + validate_update_ddex_track(params, existing_track) + deleted_track = copy_record( existing_track, params.block_number, diff --git a/packages/discovery-provider/src/tasks/entity_manager/utils.py b/packages/discovery-provider/src/tasks/entity_manager/utils.py index 59e05b6d633..5aa72550a93 100644 --- a/packages/discovery-provider/src/tasks/entity_manager/utils.py +++ b/packages/discovery-provider/src/tasks/entity_manager/utils.py @@ -1,4 +1,5 @@ import json +import os from datetime import datetime from enum import Enum from typing import Dict, List, Literal, Set, Tuple, TypedDict, Union @@ -479,3 +480,11 @@ def get_address_from_signature(signature): message_hash, signature=signature["signature"] ) return app_address.lower() + + +def is_ddex_signer(signer): + # TODO read from a table in the db after implementing UI to register a DDEX node + ddex_apps = os.getenv("audius_ddex_apps") + if ddex_apps: + return signer.removeprefix("0x") in ddex_apps.split(",") + return False diff --git a/packages/discovery-provider/src/tasks/metadata.py b/packages/discovery-provider/src/tasks/metadata.py index 7cf8c940fea..c93cafb3848 100644 --- a/packages/discovery-provider/src/tasks/metadata.py +++ b/packages/discovery-provider/src/tasks/metadata.py @@ -76,6 +76,7 @@ class TrackMetadata(TypedDict): is_playlist_upload: Optional[bool] ai_attribution_user_id: Optional[int] placement_hosts: Optional[str] + ddex_app: Optional[str] track_metadata_format: TrackMetadata = { @@ -122,6 +123,7 @@ class TrackMetadata(TypedDict): "is_playlist_upload": False, "ai_attribution_user_id": None, "placement_hosts": None, + "ddex_app": None, } # Required format for user metadata retrieved from the content system @@ -157,6 +159,7 @@ class PlaylistMetadata(TypedDict): is_image_autogenerated: Optional[bool] is_stream_gated: Optional[bool] stream_conditions: Optional[Any] + ddex_app: Optional[str] playlist_metadata_format: PlaylistMetadata = { @@ -170,6 +173,7 @@ class PlaylistMetadata(TypedDict): "is_image_autogenerated": None, "is_stream_gated": False, "stream_conditions": None, + "ddex_app": None, } # Updates cannot directly modify these fields via metadata diff --git a/packages/es-indexer/src/types/db.ts b/packages/es-indexer/src/types/db.ts index 3c1f5fc156c..906516556a7 100644 --- a/packages/es-indexer/src/types/db.ts +++ b/packages/es-indexer/src/types/db.ts @@ -423,6 +423,7 @@ export interface PlaylistRow { 'txhash': string; 'upc': string | null; 'updated_at': Date; + 'ddex_app': string | null; } export interface PlayRow { 'city': string | null; @@ -702,6 +703,7 @@ export interface TrackRow { 'track_segments': any; 'txhash': string; 'updated_at': Date; + 'ddex_app': string | null; } export interface TrendingParamRow { 'genre': string | null; diff --git a/packages/libs/src/api/entityManager.ts b/packages/libs/src/api/entityManager.ts index 1a3d8844004..311e7ef9e90 100644 --- a/packages/libs/src/api/entityManager.ts +++ b/packages/libs/src/api/entityManager.ts @@ -34,6 +34,7 @@ type PlaylistParam = { is_private: boolean is_album: boolean is_image_autogenerated: boolean + ddex_app?: string | null } /* @@ -225,7 +226,8 @@ export class EntityManager extends Base { description: playlist.description, is_album: playlist.is_album, is_private: playlist.is_private, - is_image_autogenerated: playlist.is_image_autogenerated + is_image_autogenerated: playlist.is_image_autogenerated, + ddex_app: playlist.ddex_app } this.creatorNode.validatePlaylistSchema(metadata) diff --git a/packages/libs/src/services/creatorNode/CreatorNode.ts b/packages/libs/src/services/creatorNode/CreatorNode.ts index d4b4e69999d..4b07acd41dd 100644 --- a/packages/libs/src/services/creatorNode/CreatorNode.ts +++ b/packages/libs/src/services/creatorNode/CreatorNode.ts @@ -36,6 +36,7 @@ export type PlaylistMetadata = { is_album: boolean is_private: boolean is_image_autogenerated: boolean + ddex_app?: string | null } export type ProgressCB = ( diff --git a/packages/libs/src/services/schemaValidator/schemas/playlistSchema.json b/packages/libs/src/services/schemaValidator/schemas/playlistSchema.json index 43478997c65..ba69c21081a 100644 --- a/packages/libs/src/services/schemaValidator/schemas/playlistSchema.json +++ b/packages/libs/src/services/schemaValidator/schemas/playlistSchema.json @@ -40,6 +40,10 @@ "is_image_autogenerated": { "type": "boolean", "default": false + }, + "ddex_app": { + "type": ["string", "null"], + "default": null } }, "required": [ diff --git a/packages/libs/src/services/schemaValidator/schemas/trackSchema.json b/packages/libs/src/services/schemaValidator/schemas/trackSchema.json index b40fdd5ec48..49985ef9888 100644 --- a/packages/libs/src/services/schemaValidator/schemas/trackSchema.json +++ b/packages/libs/src/services/schemaValidator/schemas/trackSchema.json @@ -154,6 +154,13 @@ ], "default": null }, + "ddex_app": { + "type": [ + "string", + "null" + ], + "default": null + }, "track_segments": { "type": "array", "minItems": 0, @@ -599,4 +606,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/libs/src/utils/types.ts b/packages/libs/src/utils/types.ts index 2da174a32e5..7c65717f6f6 100644 --- a/packages/libs/src/utils/types.ts +++ b/packages/libs/src/utils/types.ts @@ -162,6 +162,7 @@ export type TrackMetadata = { permalink: string audio_upload_id: Nullable preview_start_seconds: Nullable + ddex_app?: Nullable // Optional Fields is_invalid?: boolean @@ -206,4 +207,5 @@ export type CollectionMetadata = { updated_at: string activity_timestamp?: string is_image_autogenerated?: boolean + ddex_app?: Nullable } diff --git a/packages/sql-ts/res/discovery-node.ts b/packages/sql-ts/res/discovery-node.ts index 3335ac62448..35cf30375f6 100644 --- a/packages/sql-ts/res/discovery-node.ts +++ b/packages/sql-ts/res/discovery-node.ts @@ -380,6 +380,7 @@ export interface PlaylistRow { 'txhash'?: string; 'upc'?: string | null; 'updated_at': Date; + 'ddex_app': string | null; } export interface PlayRow { 'city'?: string | null; @@ -645,6 +646,7 @@ export interface TrackRow { 'track_segments': any; 'txhash'?: string; 'updated_at': Date; + 'ddex_app': string | null; } export interface TrendingParamRow { 'genre'?: string | null;