{"version":3,"file":"8430.81288ecd3c397414.js","mappings":"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","names":["clipIndex","makeCaptureClipFilename","userId","filename","safeName","Utils","urlSafeFileName","trim","toLowerCase","name","str","defaultName","length","safeFilename","urlSafeString","removeFileExt","replace","CLIP_TITLE_MAX_LENGTH_TRANSCODING","split","substring","makeSafeClipFilename","urlDate","Date","getFileExt","sortByTitle","clips","map","clip","title","sort","id","a","b","error","console","warn","Error","PAGE","ClipsCoreService","_ClipsCoreService","constructor","store","clipApi","youtubeService","this","uploadedClips","getClipIds$","select","selectClipIds","getClipEntities$","selectClipEntities","getClipLoadingIds$","selectClipLoadingIds","setClipIsApproved","projectId","isApproved","dispatch","clipActions","updateClipStore","updateClips","updateClip","updates","Array","isArray","updateClipApi","pipe","take","res","of","createClipId","username","titleId","createClipperUploadClipId","prependUsername","getClipFromStorage","key","Storage","get","then","link","src","listUserClipsFromStorage","list","deleteClipFromUserStorage","DEBUG_LOGS","remove","getClip","selectClip","getId","selectClips","ids","log","selectClipsByIds","c","createClip","cbLoadingProgress","hlsMeta","HLS_META_TRANSCODE","Clip","catch","err","errors","errorType","info","message","waitForClipTranscoded","maxIterations","iteration","Promise","reject","rejectDelay","reason","_resolve","setTimeout","bind","attempt","test","testClip","sources","p","i","uploadClip","saveVideoFileToS3","getClipsForProject","limit","nextToken","lastValueFrom","items","sortClipsRecent","getClipsForUser","getDescription","description","youtube_id","source","youtube_data","getMetadataDescription","getPoster","poster","DEFAULT_POSTER","getThumbnail","url","getPosterTimeFromPoster","sTime","lastIndexOf","parsed","parseInt","updatePosterToPosterTime","posterTime","newPoster","num","Math","floor","toString","deleteClip","throwError","i0","i1","i2","i3","factory","ɵfac","providedIn","MyStackService","_MyStackService","clipsService","mystack$","selectMyStack","currentClip$","selectMyStackCurrentClip","currentIndex$","selectMyStackCurrentIndex","stackClips$","selectMyStackClips","loadMyStack","mystackActions","selectClipErrorIds","subscribe","forEach","splitClipId","resetMyStack","addClipToMyStack","addClipIdsToMyStack","addClipsToMyStack","addAllClipToMyStack","updateTitle","updateDescription","updatePrivacy","privacy","Object","values","STACK_PRIVACY","includes","updatePoster","posterUrl","prePublishStack","stack","publishStack","addCaptureOnlineClipToMyStack","filmingDate","getDateTimeString","result","updateCurrentIndex","index","prevClip","nextClip","restartCurrentIndex","removeClipByIndex","removeClipFromMyStack","clearAllClips","reorderClips","clipIds","getClipId","reorderClipIds","addCaptureClipToMyStack","uploading","sorted","sortClipsByTitle","sortUserUploadSuccessByTitle","mystackClips","reorderedClips","filter","some","newClip","sortDate","lastModifiedDate","modified","created","environment","production","PublishStackService","_PublishStackService","stackApi","stackService","mystackService","projectService","userService","configService","sentryService","analyticsService","createStackDraft","input","extractStackUpdates","stackActions","saveStackUpdates","_this","_asyncToGenerator","Stack","isoDate","toISOString","dtePublished","stackId","autoId","appConfig","isWidgetActive","stackShareUrl","shareDomain","stackShareUrlIncludesProject","project","config","tryParseJSON","shareStacksToDomain","stackShareDomainUrl","stackShareDomainUrlIncludesProject","captureError","private","PRIVATE","PUBLIC","shareUrl","createShareUrl","playlist","order","durations","duration","getTotalDuration","dteSaved","credits","posterSrc","identityId","userIdentityId","tags","JSON","stringify","HLS_META_UPGRADE","saveUpdatesToStore","createdStack","createStack","addStack","subStacksUpdated","meMember","members","getRoleOfUserId","role","ProjectRole","Public","PROJECT_MEMBER_ROLE","OWNER","Owner","EXECUTIVE","Executive","PRODUCER","Producer","CREW","stackPublished","i4","i5","i6","i7","i8","i9","StacksService","_StacksService","stacksApi","events","coreApi","selectAllStacks$","getStacksAll","selectLoaded$","getStacksLoaded","makeId","getNextToken","group","getStacksGroupNextToken","loadFeaturedProjectStacks","loadRecentProjectStacks","loadProjectStacks","loadMoreProjectStacks","StackGroup","Featured","Recent","stacks","searchStacks","term","addStackTags","removeStackTags","addStacks","getAvatarUrl","getUserAvatarUrl","saveStackPoster","posterId","imageBlob","contentType","ext","mimeToExtension","put","level","vault","configure","AWSS3","bucket","subStacksUpdated$","watchAllStacksData$","params","sub","subscribeStacksUpdated","next","hlsSrc","parse","push","prop","value","hero","unsubscribe","complete","createPlaylistFromClipIds","orderedClips","clipId","captureMessage","createPlaylistFromClips","item","updateStack","userId$","first","updatedStack","deleteStack","incrementStackAdmin","incrementAdmin","incrementStackPublic","incrementPublic","getLoadedStacksOrUndefined","stacks$","combineLatest","loadedStacks","indexOf","getProjectStacksGroup","undefined","YoutubeService","_YoutubeService","http","googleApiKey","maxResults","queryUrl","iso8601DurationRegex","YOUTUBE_REGEX","YOUTUBE_PLAYLIST_REGEX","youtubeApiKey","getMetadataTitle","data","snippet","o","MAX_DESC_LENGTH_CLIPS","getMetadataPublishDate","publishedAt","getMetadataDuration","contentDetails","dur","parseISO8601Duration","padStart","val","slice","days","hours","minutes","seconds","iso8601Duration","matches","match","getVideoMetadata","csvIds","join","videoId","rootUrl","width","height","getYoutubeVideoIdFromUrl","getYoutubePlaylistIdFromUrl","testGetYoutubeVideoIdFromUrl","failures"],"ignoreList":[],"sourceRoot":"webpack:///","sources":["./src/app/clips/shared/clips.model.ts","./src/app/core/services/clips.service.ts","./src/app/core/services/mystack.service.ts","./src/app/core/services/publish-stack.service.ts","./src/app/core/services/stacks.service.ts","./src/app/core/services/youtube.service.ts"],"sourcesContent":["/**\n * @todo migrate from app/shared/models/clip.model.ts\n * @format\n */\n\nimport { CLIP_TITLE_MAX_LENGTH_TRANSCODING } from '@app/app.config';\nimport { Clip } from '@app/shared/models/clip.model';\nimport { Utils } from '@app/shared/utils';\n\n/** MVP-1061 ensure unique clip ids */\nlet clipIndex = 1;\n\n/**\n * take an uploaded Video filename and convert to app filename\n */\nexport const makeSafeClipFilename = (str): string => {\n  const defaultName = 'filmclip';\n  const filename = typeof str === 'string' && str.length > 0 ? str : defaultName;\n\n  let safeFilename = Utils.urlSafeString(Utils.removeFileExt(filename)).trim().toLowerCase();\n  /** MVP-1061 remove fullsizerender */\n  safeFilename = safeFilename.replace(/fullsizerender/g, '');\n\n  if (filename.length > CLIP_TITLE_MAX_LENGTH_TRANSCODING) {\n    const split = CLIP_TITLE_MAX_LENGTH_TRANSCODING / 2;\n    const front = str.substring(0, split - 1); // leave a char for the _\n    const back = str.substring(str.length - split);\n    return `${front}_${back}-${clipIndex++}`;\n  }\n  return `${safeFilename || defaultName}-${clipIndex++}`;\n};\n\n/**\n * create a filename with userId and add Date\n * @param userId userId\n * @param filename full filename.ext\n */\nexport const makeCaptureClipFilename = (userId = 'anon', filename = 'filmclip') => {\n  const sep = '_-_';\n  const safeName = Utils.urlSafeFileName(filename).trim().toLowerCase();\n  const name = makeSafeClipFilename(Utils.removeFileExt(safeName));\n  return (\n    Utils.urlSafeString(userId + sep + name + '_' + Utils.urlDate(new Date())) +\n    '.' +\n    Utils.getFileExt(safeName)\n  ).toLowerCase();\n  // return Utils.urlSafeString(username) + sep + (addDate ? Utils.shortUrlDate(new Date()) + sep : '') + name;\n};\n\nexport const sortByTitle = (clips: Partial<Clip>[]) => {\n  try {\n    const ordered = clips.map((clip) => {\n      if (!clip) {\n        return {\n          ...clip,\n          sort: '',\n        };\n      }\n      if (clip.title) {\n        return {\n          ...clip,\n          sort: clip.title.toLowerCase(),\n        };\n      }\n      return {\n        ...clip,\n        sort: clip.id.toLowerCase(),\n      };\n    });\n    return ordered.sort((a, b) => (a.sort < b.sort ? -1 : a.sort > b.sort ? 1 : 0));\n  } catch (error) {\n    console.warn(error);\n    throw new Error(`Uh oh! We ran into an issue - please try again.`);\n  }\n};\n","/** @format */\n\nimport { Injectable } from '@angular/core';\nimport { Observable, throwError, of, lastValueFrom } from 'rxjs';\nimport { map, take } from 'rxjs/operators';\nimport { Store } from '@ngrx/store';\nimport { State } from '../store/reducers';\nimport * as clipActions from '../store/actions/clips.actions';\nimport {\n  selectClipIds,\n  selectClipEntities,\n  selectClipLoadingIds,\n  selectClip,\n  selectClipsByIds,\n  getId,\n} from '@store/selectors/clips.selectors';\n\nimport { Storage } from 'aws-amplify';\nimport { Clip, DEFAULT_POSTER, sortClipsRecent, HLS_META_TRANSCODE } from '@app/shared/models/clip.model';\nimport { Utils } from '@app/shared/utils';\nimport { ClipsApiService, S3UploadResponse } from '../api/clips-api.service';\nimport { UpdateParam } from '../api/api-types';\nimport { YoutubeService } from './youtube.service';\n// import _partition from 'lodash/partition';\n\nconst DEBUG_LOGS = false;\n\nconst UPLOAD_TIMEOUT_POST_TRANSCODE_MS = 30000; // wait ms before \"_AFTER_UPLOAD\" actions (20sec)\n// const CREATE_CLIP_AFTER_UPLOAD: boolean = false; // after upload, get the new Clip from API and add to store\n// export const ADD_TO_MYSTACK_AFTER_UPLOAD: boolean = false; // if CreateClip, also add it to MyStack\n\n/**\n * chars to keep of filename, to avoid failures in transcoding\n */\nconst MAX_TITLE_LENGTH_CLIP_TITLE = 32;\n\n/**\n * take an uploaded Video filename and convert to app filename\n */\nexport const makeNewFilename = (str): string => {\n  const defaultName = 'filmclip';\n  const filename = typeof str === 'string' && str.length > 0 ? str : defaultName;\n\n  if (filename.length > MAX_TITLE_LENGTH_CLIP_TITLE) {\n    const split = MAX_TITLE_LENGTH_CLIP_TITLE / 2;\n    const front = str.substring(0, split - 1); // leave a char for the _\n    const back = str.substring(str.length - split);\n    return `${front}_${back}`;\n  }\n  return filename;\n};\n\nconst PAGE = '[ClipsCoreService]';\n\n/**\n * @todo move to Clips Feature\n */\n@Injectable({\n  providedIn: 'root',\n})\nexport class ClipsCoreService {\n  uploadedClips: string[] = []; // todo: move to store, but for now, keep them\n\n  /*\n   * ngrx Store connections\n   * why are Entity ids <string[] | number[]> ?\n   */\n  getClipIds$: Observable<string[] | number[]> = this.store.select(selectClipIds);\n  getClipEntities$: Observable<{ [id: string]: Clip }> = this.store.select(selectClipEntities);\n  getClipLoadingIds$: Observable<string[]> = this.store.select(selectClipLoadingIds);\n\n  constructor(private store: Store<State>, private clipApi: ClipsApiService, private youtubeService: YoutubeService) {}\n\n  setClipIsApproved({\n    projectId,\n    id,\n    userId,\n    isApproved = true,\n  }: {\n    projectId: string;\n    id: string;\n    userId: string;\n    isApproved: boolean;\n  }) {\n    this.store.dispatch(clipActions.setApproved({ projectId, id, userId, isApproved }));\n  }\n\n  /**\n   * Add / Update Clip in Store\n   */\n  updateClipStore(clip: Clip): void {\n    DEBUG_LOGS && console.log(`${PAGE} update Clip store`, clip);\n    this.store.dispatch(clipActions.updateClip({ clip }));\n  }\n  updateClips(clips: Clip[]): void {\n    DEBUG_LOGS && console.log(`${PAGE} update Clip store`, clips);\n    this.store.dispatch(clipActions.addClips({ clips }));\n  }\n\n  updateClip(clip: Clip, updates: UpdateParam[] = null): Observable<Clip | string> {\n    if (Array.isArray(updates) && updates.length > 0) {\n      DEBUG_LOGS && console.log(`${PAGE} update Clip api`, updates, clip);\n      return this.updateClipApi(clip, updates).pipe(\n        take(1),\n        map((res) => {\n          DEBUG_LOGS && console.log(`${PAGE} update Clip store`, res);\n          this.store.dispatch(clipActions.updateClip({ clip: res }));\n          return res;\n        })\n      );\n    } else {\n      DEBUG_LOGS && console.log(`${PAGE} update Clip store`, clip);\n      this.store.dispatch(clipActions.updateClip({ clip }));\n      return of('No API Changes, updated store.');\n    }\n  }\n\n  /**\n   * create a string id\n   * @param username\n   * @param titleId\n   */\n  createClipId(username, titleId) {\n    return Utils.urlSafeString(username + '_-_' + titleId + '_' + Utils.urlDate(new Date()));\n  }\n\n  /**\n   * create a string id for uploading temp video for clipper\n   * @param username\n   * @param titleId\n   */\n  createClipperUploadClipId(username, titleId, prependUsername = true) {\n    return Utils.urlSafeString((prependUsername ? username + '_-_' : '') + titleId + '_' + Utils.urlDate(new Date()));\n  }\n\n  /**\n   * Amplify Storage (S3) access\n   * Currently UNUSED\n   */\n  getClipFromStorage(key: string) {\n    return Storage.get(key).then((link) => ({\n      link: link as string,\n      src: key,\n    }));\n  }\n  /**\n   * Amplify Storage (S3) access\n   * Currently UNUSED\n   * now path is: filestackConfig.storeTo.path\n   */\n  listUserClipsFromStorage(username: string) {\n    return Storage.list(`uploads/videos/${username}_-_`).then((res) => res);\n  }\n\n  /**\n   * Delete Clip from Amplify Storage (S3)\n   * @see my-stack-capture-item.doUpload()\n   */\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  deleteClipFromUserStorage(key: string): Promise<any> {\n    DEBUG_LOGS && console.log(`${PAGE} deleteClipFromUserStorage`, key);\n    return Storage.remove(key).then((res) => {\n      DEBUG_LOGS && console.log(`${PAGE} deleteClipFromUserStorage`, res);\n      return res;\n    });\n  }\n\n  /**\n   * Get Clip By ID\n   */\n  getClip(projectId: string, id: string): Observable<Clip> {\n    this.store.dispatch(clipActions.load({ projectId, id })); // ensure it's in the store (Effect)\n    return this.store.select(selectClip(getId(projectId, id)));\n  }\n\n  /**\n   * Get Clips By IDs\n   * select from Store and return vals as Observable\n   */\n  selectClips(ids: { projectId: string; id: string }[]): Observable<Clip[]> {\n    // DEBUG_LOGS &&\n    console.log(`${PAGE} selectClips:`, { ids });\n    this.store.dispatch(clipActions.loadBatchIds({ ids }));\n    return this.store.select(selectClipsByIds(ids.map((c) => getId(c.projectId, c.id))));\n  }\n\n  /**\n   * upload clip and add to store\n   * @param clip\n   */\n  createClip(clip: Clip, cbLoadingProgress = null): Promise<Clip> {\n    DEBUG_LOGS && console.log(`${PAGE} createClip`, clip);\n    // TODO: first, check if it already exists\n\n    // add the new HLS transcoding instruction\n    if (!clip.hlsMeta) {\n      clip.hlsMeta = {};\n    }\n\n    clip.hlsMeta = {\n      ...clip.hlsMeta,\n      ...HLS_META_TRANSCODE,\n    };\n\n    return this.clipApi\n      .createClip(clip, cbLoadingProgress)\n      .then((res) => {\n        DEBUG_LOGS && console.log(`${PAGE} createClip res:`, res);\n        return new Clip(res);\n      })\n      .catch((err) => {\n        if (\n          err &&\n          Array.isArray(err.errors) &&\n          err.errors.length > 0 &&\n          err.errors[0] &&\n          err.errors[0].errorType &&\n          err.errors[0].errorType === 'DynamoDB:ConditionalCheckFailedException'\n        ) {\n          console.info(`${PAGE} createClip already Exists:`, err);\n          return clip;\n        }\n        console.error(`${PAGE} createClip err:`, err);\n        if (err && Array.isArray(err.errors) && err.errors.length > 0 && err.errors[0].message) {\n          throw err.errors[0].message;\n        }\n        throw err;\n      });\n  }\n\n  // private _checkTranscodedTimeout;\n  // private _checkTranscodedIteration: number = 0; // just for dev logging\n  /**\n   * UPLOAD_TIMEOUT_POST_TRANSCODE_ACTIONS - tried delaying by up to 3 seconds,\n   * but it takes around 10sec before transcode complete and then s3 moved\n   *\n   * @todo: graphql subscription (or Pub-Sub) to notify when done\n   */\n  waitForClipTranscoded(clip: Clip, maxIterations = 7): Promise<Clip> {\n    const delay = UPLOAD_TIMEOUT_POST_TRANSCODE_MS;\n    let iteration = 0;\n\n    if (!clip.projectId || !clip.id) {\n      return Promise.reject('Missing required args');\n    }\n\n    // v2 refactor , based on: https://stackoverflow.com/questions/38213668/promise-retry-design-patterns\n    // consider refactor to Util using rxjs retryWhen https://www.learnrxjs.io/operators/error_handling/retrywhen.html\n\n    const processResult = (res) => {\n      DEBUG_LOGS && console.log(`Clip Transcoded in ${(iteration * delay) / 1000} seconds (${iteration} times)`);\n      return res;\n    };\n    const errorHandler = (err) => {\n      console.warn(err);\n      throw new Error(`Tried ${iteration} times - Transcode not done yet (${(iteration * delay) / 1000} seconds)`);\n    };\n\n    const rejectDelay = (reason) =>\n      new Promise((_resolve, reject) => {\n        setTimeout(reject.bind(null, reason), delay);\n      });\n\n    const attempt = () => {\n      iteration++;\n      DEBUG_LOGS && console.log(`${PAGE} checkClipTranscoded -> getClip (iter=${iteration})...`);\n      return this.clipApi.getClip(clip.projectId, clip.id);\n    };\n\n    const test = (testClip) => {\n      DEBUG_LOGS && console.log(`${PAGE} testing clip transcoded (iter=${iteration})...`);\n      const transcodeSuccess = testClip && Array.isArray(testClip.sources) && testClip.sources.length > 0;\n      if (transcodeSuccess) {\n        return testClip;\n      } else {\n        // do it again...\n        throw new Error(`Clip not yet transcoded. (iter=${iteration})`);\n      }\n    };\n\n    let p: Promise<void> = Promise.reject();\n    for (let i = 0; i < maxIterations; i++) {\n      p = p.catch(attempt).then(test).catch(rejectDelay);\n    }\n    return p.then(processResult).catch(errorHandler);\n  }\n\n  /**\n   * just upload clip\n   * @param clip\n   */\n  uploadClip(clip: Clip, cbLoadingProgress = null): Promise<S3UploadResponse> {\n    DEBUG_LOGS && console.log(`${PAGE} uploadClip`, clip);\n\n    return this.clipApi.saveVideoFileToS3(clip, cbLoadingProgress).then((res) => {\n      DEBUG_LOGS && console.log(`${PAGE} saveVideoFileToS3 res:`, res);\n      if (res.error) {\n        throw new Error(res.error);\n      }\n      return res;\n    });\n  }\n\n  /**\n   * graphql:\n   * getClipsByProject(projectId: ID!, filter: TableClipFilterInput, limit: Int, nextToken: String ): ClipConnection\n   */\n  getClipsForProject(\n    projectId: string,\n    limit = 50,\n    nextToken = null\n  ): Promise<{ clips: Clip[]; nextToken: string; error?: string }> {\n    // appsync: getClipsByProject(projectId: ID!, filter: TableClipFilterInput, limit: Int, nextToken: String ): ClipConnection\n    return lastValueFrom(this.clipApi.getClipsForProject(projectId, limit, nextToken))\n      .then((res) => {\n        DEBUG_LOGS && console.log(`${PAGE} getClipsForProject`, res);\n        if (res && Array.isArray(res.items)) {\n          const clips: Clip[] = res.items.map((clip) => new Clip(clip)).sort(sortClipsRecent);\n          this.store.dispatch(clipActions.addClips({ clips }));\n          return { clips, nextToken: res.nextToken };\n        } else {\n          console.warn(`${PAGE} getClipsForProject !res || !Array??`, res);\n          return { clips: [], nextToken: '', error: 'items_not_array' };\n        }\n      })\n      .catch((err) => {\n        console.error(`${PAGE} getClipsForProject Error`, err);\n        throw err;\n        // return { clips: [], nextToken: '',  error: (err && err.message) ? err.message : 'unknown_error' };\n      });\n  }\n  // Simon's branch:\n  //   ).pipe(\n  //     take(1)\n  //   ).subscribe(res => console.log(`${PAGE} loadClip res:`,res));\n  // }\n\n  getClipsForUser(\n    userId: string,\n    limit = 50,\n    nextToken = null\n  ): Promise<{ clips: Clip[]; nextToken: string; error?: string }> {\n    return this.clipApi\n      .getClipsForUser(userId, limit, nextToken)\n      .then((res) => {\n        DEBUG_LOGS && console.log(`${PAGE} getClipsForUser`, res);\n\n        let clips: Clip[] = [];\n        if (res && Array.isArray(res.items)) {\n          clips = res.items.map((clip) => new Clip(clip)).sort(sortClipsRecent);\n          this.store.dispatch(clipActions.addClips({ clips }));\n          return { clips, nextToken: res.nextToken };\n        } else {\n          console.warn(`${PAGE} getClipsForUser !res || !Array??`, res);\n          return { clips: [], nextToken: '', error: 'items_not_array' };\n        }\n      })\n      .catch((err) => {\n        console.error(`${PAGE} getClipsForUser Caught:`, err);\n        if (err && Array.isArray(err.errors) && err.errors.length > 0) {\n          console.error(`${PAGE} getClipsForUser Error:`, err.errors[0]);\n        }\n        throw err;\n      });\n  }\n\n  /**\n   * Get Clip Description or default res if not found\n   * @param res (string): return value if no description found\n   */\n  getDescription(clip: Clip, res: string = '') {\n    if (!clip) {\n      return res;\n    }\n    if (clip.description) {\n      return clip.description;\n    } else if (clip.youtube_id && clip.source && clip.source.youtube_data) {\n      return this.youtubeService.getMetadataDescription(clip.source.youtube_data) || '';\n    } else {\n      return res;\n    }\n  }\n\n  /**\n   * @deprecated use app/clips/shared/services/clips.service instead\n   */\n  getPoster(clip: Clip, res: string = 'default') {\n    let poster = DEFAULT_POSTER;\n    if (clip) {\n      if (clip.poster) {\n        poster = clip.poster;\n      } else if (clip.youtube_id) {\n        poster = this.youtubeService.getThumbnail(clip.youtube_id, res).url;\n      }\n    }\n    return poster;\n  }\n\n  /**\n   * @deprecated use method in @app/clips/shared/services/clips.service\n   * Take the poster string and return poster time from the transcoded name\n   * note that the poster is 1-based, so we need to subtract 1\n   */\n  getPosterTimeFromPoster(clip: Clip) {\n    if (clip && clip.poster) {\n      const sNum = clip.poster.substring(clip.poster.lastIndexOf('--'), clip.poster.lastIndexOf('.'));\n      const sTime = sNum.replace(/-/g, '').replace(/0/gi, '');\n      try {\n        const parsed = parseInt(sTime, 10);\n        return parsed > 0 ? parsed - 1 : 0;\n      } catch (error) {\n        return 0;\n      }\n    }\n    return 0;\n  }\n\n  /**\n   * @deprecated use method in clipsModule/shared/services/clips.service\n   * Update the poster image to match the new posterTime\n   *\n   * TODO: verify it exists in s3\n   * potential ref: https://stackoverflow.com/questions/9815762/detect-when-an-image-fails-to-load-in-javascript\n   *\n   * https://videos.filmstacker.com/public/filmstacker-dev/jd_-_jd-note_202203021348/jd_-_jd-note_202203021348_thumb.0000000.jpg\n   */\n  updatePosterToPosterTime(clip: Clip, posterTime: number) {\n    if (clip && clip.poster) {\n      let newPoster = clip.poster;\n      // let posterName = Utils.removeFileExt(clip.poster);\n      const num = clip.poster.substring(clip.poster.lastIndexOf('--'), clip.poster.lastIndexOf('.'));\n      if (num.length > 2 && typeof posterTime === 'number') {\n        // Note that posterTime is index 0, but the poster images were transcoded with index of 1\n        let sTime = (Math.floor(posterTime) + 1).toString();\n        // make a string equal to num.length - 2 (not counting the dashes)\n        while (sTime.length < num.length - 2) sTime = '0' + sTime;\n\n        newPoster = newPoster.replace(num, `--${sTime}`);\n\n        console.info(\n          `${PAGE} change clip.poster (TODO: verify it exists in s3) posterTime: ${posterTime} to:`,\n          newPoster\n        );\n      } else {\n        console.warn(`${PAGE} clip.poster - unable to find poster number substr?`, clip.poster);\n      }\n      return newPoster;\n    } else {\n      console.warn(`${PAGE} missing clip.poster - unable to change`, clip);\n      return null;\n    }\n  }\n\n  /**\n   * upload clip and add to store\n   * @param clip\n   */\n  deleteClip(clip: Clip): Promise<Partial<Clip>> {\n    DEBUG_LOGS && console.log(`${PAGE} deleteClip`, clip);\n    return this.clipApi\n      .deleteClip(clip.projectId, clip.id)\n      .then((res) => {\n        DEBUG_LOGS && console.log(`${PAGE} deleteClip res:`, res);\n        this.store.dispatch(clipActions.deleteClip({ clip }));\n        // return 'Clip deleted';\n        return res;\n      })\n      .catch((err) => {\n        console.error(`${PAGE} deleteClip err:`, err);\n        throw err;\n      });\n  }\n\n  /**\n   * Update a Clip - supply the Clip and an array of UpdateParam{ prop: string, value: any }\n   * @param clip\n   * @param updates\n   * @returns the changed props + ids\n   */\n  private updateClipApi(clip: Clip, updates: UpdateParam[]): Observable<Clip> {\n    // could return a Promise from api if desired...\n    if (!clip || !clip.projectId || !clip.id) {\n      return throwError(() => new Error('Missing Required Clip ids'));\n    }\n    return this.clipApi.updateClip(clip, updates);\n  }\n}\n","/** @format */\n\nimport { Injectable } from '@angular/core';\nimport { Observable, take } from 'rxjs';\n\nimport { Store } from '@ngrx/store';\nimport { State } from '@store/reducers';\nimport {\n  selectMyStack,\n  selectMyStackClips,\n  selectMyStackCurrentClip,\n  selectMyStackCurrentIndex,\n} from '@store/selectors/mystack.selectors';\nimport { selectClipErrorIds } from '@store/selectors/clips.selectors';\nimport * as mystackActions from '@store/actions/mystack.actions';\nimport { getId as getClipId, splitId as splitClipId } from '@store/selectors/clips.selectors';\n\nimport { ClipsCoreService } from '@app/core/services/clips.service';\nimport { Clip } from '@app/shared/models/clip.model';\nimport { STACK_PRIVACY, Stack } from '@app/shared/models/stack.model';\nimport { Utils } from '@app/shared/utils';\nimport { sortByTitle as sortClipsByTitle } from '@app/clips/shared/clips.model';\nimport { environment } from 'src/environments/environment';\n\nconst DEBUG_LOGS = false;\nconst PAGE = '[MyStackService]';\n\nexport interface ReorderStackClips {\n  from: number;\n  to: number;\n  clips: Clip[];\n}\n\n/**\n * serves as an intermediary between the application store and the rest of the application\n * https://onehungrymind.com/handle-multiple-angular-2-models-ngrx-computed-observables/\n */\n@Injectable({\n  providedIn: 'root',\n})\nexport class MyStackService {\n  /*\n   * ngrx Store connections\n   */\n  mystack$: Observable<Stack> = this.store.select(selectMyStack);\n  currentClip$: Observable<Clip> = this.store.select(selectMyStackCurrentClip);\n  currentIndex$: Observable<number> = this.store.select(selectMyStackCurrentIndex);\n  stackClips$: Observable<Clip[]> = this.store.select(selectMyStackClips);\n\n  constructor(private store: Store<State>, private clipsService: ClipsCoreService) {}\n\n  // selectMyStack$: Observable<Stack> = this.store.select(fromStore.getMyStack);\n  // selectClips$: Observable<{[id:string]: Clip}> = this.store.select(fromStore.getClipEntities);\n  // combinedStackClips$: Observable<[Stack[], {[id:string]: Clip}]> = Observable.combineLatest(this.selectStacks$, this.selectClips$);\n\n  loadMyStack() {\n    // doing this in app.component instead\n    setTimeout(() => {\n      this.store.dispatch(mystackActions.load());\n    }, 700);\n\n    // should this move to app.component too?\n    this.store.select(selectClipErrorIds).subscribe((ids) => {\n      try {\n        if (Array.isArray(ids) && ids.length > 0) {\n          console.log(`mystack selectClipErrorIds`, ids);\n          ids.forEach((id) => {\n            this.store.dispatch(mystackActions.removeClip({ clip: new Clip(splitClipId(id)) }));\n          });\n        }\n      } catch (error) {\n        console.warn(`mystack selectClipErrorIds caught`, error);\n      }\n    });\n  }\n\n  /** Actions */\n\n  resetMyStack() {\n    this.store.dispatch(mystackActions.reset());\n  }\n\n  /**\n   * @deprecated - use addClipsToMyStack()\n   */\n  addClipToMyStack(clip: Clip) {\n    this.store.dispatch(mystackActions.addClip({ clip }));\n    // this.events.publish(EventActions.FLASH_MYSTACK); // now done in mystack.effects\n  }\n\n  /**\n   * Add clips to mystack by ids, Effect to ensure loaded\n   * FLASH_MYSTACK in mystack.effects\n   */\n  addClipIdsToMyStack(ids: { projectId: string; id: string }[]) {\n    DEBUG_LOGS && console.log(`${PAGE} addClipIdsToMyStack:`, ids);\n    this.store.dispatch(mystackActions.addClipIds({ ids }));\n  }\n  addClipsToMyStack(clips: Clip[]) {\n    // DEBUG_LOGS && console.log(`${PAGE} addClipsToMyStack:`, clips);\n    return this.addClipIdsToMyStack(clips.map((clip) => ({ projectId: clip.projectId, id: clip.id })));\n  }\n\n  addAllClipToMyStack(clips: Clip[]) {\n    if (Array.isArray(clips) && clips.length > 0) {\n      /**\n       * @todo mystackActions.addMultipleClips\n       */\n      clips.forEach((clip) => {\n        this.store.dispatch(mystackActions.addClip({ clip }));\n      });\n      // this.events.publish(EventActions.FLASH_MYSTACK); // now done in mystack.effects\n    } else {\n      console.log(`${PAGE} no clips`);\n    }\n  }\n\n  updateTitle(title: string) {\n    this.store.dispatch(mystackActions.updateTitle({ title }));\n  }\n  updateDescription(description: string) {\n    this.store.dispatch(mystackActions.updateDescription({ description }));\n  }\n  updatePrivacy(privacy: STACK_PRIVACY) {\n    if (Object.values(STACK_PRIVACY).includes(privacy)) {\n      this.store.dispatch(mystackActions.updatePrivacy({ privacy }));\n    }\n  }\n  updatePoster(posterUrl: string) {\n    this.store.dispatch(mystackActions.updatePoster({ poster: posterUrl }));\n  }\n  prePublishStack(stack: Stack) {\n    this.store.dispatch(mystackActions.prePublishStack({ stack }));\n  }\n  publishStack(stack: Stack) {\n    this.store.dispatch(mystackActions.publishStack({ stack }));\n  }\n\n  /**\n   * Handle a clip from Capture ONLINE\n   */\n  addCaptureOnlineClipToMyStack(clip: Clip) {\n    DEBUG_LOGS && console.log(`${PAGE} addCaptureOnlineClipToMyStack:`, clip);\n\n    try {\n      if (clip && typeof clip.filmingDate !== 'string') {\n        clip.filmingDate = Utils.getDateTimeString(clip.filmingDate);\n      }\n    } catch (error) {\n      console.warn(error);\n      clip.filmingDate = Utils.getDateTimeString(clip.filmingDate);\n    }\n\n    return this.clipsService.createClip(clip).then((result) => {\n      console.log(`${PAGE} createClip res:`, result);\n      this.store.dispatch(mystackActions.addClip({ clip: result }));\n      // this.events.publish(EventActions.FLASH_MYSTACK); // now done in mystack.effects\n    });\n  }\n\n  updateCurrentIndex(index: number) {\n    this.store.dispatch(mystackActions.currentIndex({ index }));\n  }\n\n  prevClip() {\n    this.store.dispatch(mystackActions.prevClip());\n  }\n  nextClip() {\n    this.store.dispatch(mystackActions.nextClip());\n  }\n  restartCurrentIndex() {\n    this.store.dispatch(mystackActions.currentIndex({ index: 0 }));\n  }\n\n  removeClipByIndex(index: number) {\n    this.store.dispatch(mystackActions.removeClipByIndex({ index }));\n  }\n\n  removeClipFromMyStack(clip: Clip) {\n    this.store.dispatch(mystackActions.removeClip({ clip }));\n  }\n\n  clearAllClips() {\n    this.store.dispatch(mystackActions.clearAllClips());\n  }\n\n  //  * @deprecated we don't need from/to here...we were not using them anyways...\n  // reorderClipsFromTo({ from, to, clips }: ReorderStackClips) {\n  //   DEBUG_LOGS && console.log(`${PAGE} reorderClips`, { from, to, clips });\n  //   if (!Array.isArray(clips)) {\n  //     return console.warn(`${PAGE} reorderClips clip !array!`);\n  //   }\n  //   const clipIds = clips.map((clip) => getClipId(clip.projectId, clip.id));\n  //   this.store.dispatch(mystackActions.reorderClipIds({ clipIds }));\n  // }\n\n  /**\n   * Reorder from mystack\n   */\n  reorderClips(clips: Partial<Clip>[]) {\n    // DEBUG_LOGS && console.log(`${PAGE} reorderClips`, { clips });\n    if (!Array.isArray(clips)) {\n      return console.warn(`${PAGE} reorderClips !array!`);\n    }\n    const clipIds = clips.map((clip) => getClipId(clip.projectId, clip.id));\n    this.reorderClipIds(clipIds);\n  }\n  reorderClipIds(clipIds: string[]) {\n    DEBUG_LOGS && console.log(`${PAGE} reorderClipIds`, { clipIds });\n    if (!Array.isArray(clipIds)) {\n      return console.warn(`${PAGE} reorderClipIds !array!`);\n    }\n    this.store.dispatch(mystackActions.reorderClipIds({ clipIds }));\n  }\n\n  /**\n   * @deprecated store is updated directly in clips.service\n   * Handle a clip from Capture\n   */\n  addCaptureClipToMyStack(clip: Clip) {\n    DEBUG_LOGS && console.log(`${PAGE} addCaptureClipToMyStack:`, clip);\n    /**\n     * @todo remove this once the mystack-capture-item has been refactored.\n     */\n    clip.source.uploading = true;\n\n    this.store.dispatch(mystackActions.addClip({ clip }));\n    // this.events.publish(EventActions.FLASH_MYSTACK); // now done in mystack.effects\n  }\n\n  sortByTitle(clips: Partial<Clip>[]) {\n    try {\n      const sorted = sortClipsByTitle(clips);\n      this.reorderClips(sorted);\n      return sorted;\n    } catch (error) {\n      console.warn(error);\n      throw new Error(`Uh oh! We ran into an issue - please try again.`);\n    }\n  }\n\n  /**\n   * find filesUploaded at the end of mystack,\n   * sort those, leave the ones that have been dragged already\n   */\n  sortUserUploadSuccessByTitle(clips: Partial<Clip>[]) {\n    this.store\n      .select(selectMyStackClips)\n      .pipe(take(1))\n      .subscribe((mystackClips) => {\n        console.log('sortUserUploadSuccessByTitle', { mystackClips, clips });\n        try {\n          const sorted = sortClipsByTitle(clips);\n          // filter out the clips we are adding, assuming they have not been moved we might replace by last index?\n          const oldStackClips = mystackClips.filter(\n            (clip) => clip?.id && !sorted.some((newClip) => newClip.id === clip.id)\n          );\n          const reorderedClips = [...oldStackClips, ...sorted];\n          DEBUG_LOGS &&\n            console.log(`sortUserUploadSuccessByTitle...`, { reorderedClips, oldStackClips, sorted, clips });\n          this.reorderClips(reorderedClips);\n          return reorderedClips;\n        } catch (error) {\n          console.warn(error);\n          throw new Error(`Uh oh! We ran into an issue - please try again.`);\n        }\n      });\n  }\n\n  sortDate(clips: Partial<Clip>[]) {\n    try {\n      const ordered = clips.map((clip) => {\n        if (!clip) {\n          return {\n            ...clip,\n            sortDate: '',\n          };\n        }\n        if (clip.source && clip.source.lastModifiedDate) {\n          return {\n            ...clip,\n            sortDate: clip.source.lastModifiedDate,\n          };\n        }\n        return {\n          ...clip,\n          sortDate: clip.filmingDate || clip.modified || clip.created,\n        };\n      });\n      // '2007-01-17T08:00:00Z' < '2008-01-17T08:00:00Z' === true\n      const sorted = ordered.sort((a, b) => (a.sortDate < b.sortDate ? -1 : a.sortDate > b.sortDate ? 1 : 0));\n      !environment.production && console.log(`sortDate...`, { sorted, clips });\n      this.reorderClips(sorted);\n      return sorted;\n    } catch (error) {\n      console.warn(error);\n      throw new Error(`Uh oh! We ran into an issue - please try again.`);\n    }\n  }\n}\n","/** @format */\n\nimport { Injectable } from '@angular/core';\nimport { Utils } from '@app/shared/utils';\nimport {\n  extractStackUpdates,\n  Stack,\n  StackDraftPublishInput,\n  StackPublishInput,\n  STACK_PRIVACY,\n} from '@app/shared/models/stack.model';\nimport { HLS_META_UPGRADE } from '@app/shared/models/clip.model';\nimport { StacksService } from './stacks.service';\nimport { MyStackService } from './mystack.service';\nimport { UserService } from './user.service';\nimport { SentryService } from './sentry.service';\nimport { StacksApiService } from '../api/stacks-api.service';\nimport { ConfigService } from '@app/core/config/config.service';\nimport { PROJECT_MEMBER_ROLE } from '@app/projects/shared/project.model';\nimport * as stackActions from '@store/actions/stacks.actions';\nimport * as mystackActions from '@store/actions/mystack.actions';\nimport { Store } from '@ngrx/store';\nimport { State } from '@store/reducers';\nimport { AnalyticsService } from './analytics/analytics.service';\nimport { ProjectService } from '@app/projects/shared/services/project.service';\nimport { ProjectRole } from './analytics/google-analytics.service';\n\nconst DEBUG_LOGS = false;\nconst PAGE = '[PublishService]';\n\n/**\n * create shareUrl similar to\n * https://stacks.filmstacker.com/projectId/Cody-Sheehy-AKA-Masterstcker_CAP-Water-is-Critical-To-Tucson_2016216\n * with an index.html created by aws lambda based on the uploaded stackUrl.json\n */\n@Injectable({\n  providedIn: 'root',\n})\nexport class PublishStackService {\n  constructor(\n    private stackApi: StacksApiService,\n    private stackService: StacksService,\n    private mystackService: MyStackService,\n    private projectService: ProjectService,\n    private userService: UserService,\n    private configService: ConfigService,\n    private sentryService: SentryService,\n    private analyticsService: AnalyticsService,\n    private store: Store<State>\n  ) {}\n\n  /**\n   * MVP-995 Save Draft\n   * if there are already clips, save those too\n   * @todo we need an ID for the navigation\n   */\n  createStackDraft(input: StackDraftPublishInput): Stack {\n    DEBUG_LOGS && console.log(`${PAGE} saveStackDraft input:`, input);\n    if (!input.projectId) {\n      throw new Error('No projectId found - please add clips and try again.');\n    }\n    const { stack } = extractStackUpdates(input, DEBUG_LOGS);\n    // here, instead of using \"mystack\" store, use stacks, set currentEditStackId\n    this.store.dispatch(stackActions.addDraft({ stack }));\n    DEBUG_LOGS && console.log(`${PAGE} createStackDraft addDraft...`, stack);\n    return stack;\n  }\n  saveStackUpdates(input: StackPublishInput | Partial<Stack>): Stack {\n    DEBUG_LOGS && console.log(`${PAGE} saveStackUpdates input:`, input);\n    if (!input.projectId) {\n      throw new Error('No projectId found - please add clips and try again.');\n    }\n    const { stack, updates } = extractStackUpdates(input, DEBUG_LOGS);\n    this.store.dispatch(mystackActions.updateMyStack({ stack, updates }));\n    // doing the stack update in effect\n    // this.store.dispatch(stackActions.update({ stack, updates }));\n    return stack;\n  }\n\n  /**\n   * Called by stack-form-details\n   * could be a save stack process as well\n   */\n  async publishStack(input: StackPublishInput | Stack): Promise<Stack> {\n    // save the views and shares on re-publish by passing the input to constructor MVP-1265\n    const stack = new Stack(input);\n    DEBUG_LOGS && console.log(`${PAGE} PublishStack input:`, input);\n\n    if ((input?.clips ?? []).length === 0) {\n      /*\n        Moved this check into the caller to avoid error..\n        Error: Uncaught (in promise): Error: No playlist found - please add clips and try again.\n      */\n      throw new Error('No playlist found - please add clips and try again.');\n    }\n\n    const now = new Date();\n    const isoDate = now.toISOString();\n    stack.dtePublished = isoDate;\n\n    stack.projectId = input.projectId;\n    stack.stackId = input.stackId ? input.stackId : Utils.autoId(input.title, input.userId);\n\n    /**\n     * Handle Config for Widget\n     */\n    try {\n      const appConfig = await this.configService.appConfig; //.then((appConfig) => {\n      if (appConfig.isWidgetActive) {\n        if (appConfig.stackShareUrl) {\n          if (appConfig.stackShareUrlIncludesProject) {\n            stack.shareDomain = `${appConfig.stackShareUrl}${stack.projectId}/${stack.stackId}`;\n          } else {\n            stack.shareDomain = `${appConfig.stackShareUrl}${stack.stackId}`;\n          }\n        }\n      } else if ((input as StackPublishInput).project && (input as StackPublishInput).project.config) {\n        /**\n         * Check Project config for widget too\n         */\n        const config = Utils.tryParseJSON((input as StackPublishInput).project.config);\n        DEBUG_LOGS && console.log(`${PAGE} PublishStack project config:`, { config });\n        if (config && config.shareStacksToDomain && config.stackShareDomainUrl) {\n          if (config.stackShareDomainUrlIncludesProject) {\n            stack.shareDomain = `${config.stackShareDomainUrl}${stack.projectId}/${stack.stackId}`;\n          } else {\n            stack.shareDomain = `${config.stackShareDomainUrl}${stack.stackId}`;\n          }\n        }\n      }\n      // });\n    } catch (error) {\n      this.sentryService.captureError(error);\n    }\n\n    try {\n      stack.title = input.title.trim();\n      stack.description = (input.description || '').trim();\n      stack.private = input.private;\n      stack.privacy = input.private ? STACK_PRIVACY.PRIVATE : STACK_PRIVACY.PUBLIC;\n      stack.shareUrl = this.stackApi.createShareUrl(stack.projectId, stack.stackId);\n      stack.playlist = input.clips.map((clip, index) => ({\n        projectId: clip.projectId,\n        id: clip.id,\n        order: index,\n      }));\n      // calc the total duration for the stack\n      const durations = input.clips.filter((clip) => clip && clip.duration).map((clip) => clip.duration);\n      stack.duration = Utils.getTotalDuration(durations);\n\n      /** this is the key to make it a DRAFT */\n      stack.dtePublished = isoDate;\n      stack.dteSaved = isoDate;\n      stack.userId = input.userId;\n      stack.credits = input.credits;\n      // handle custom poster upload - might have an auth token in the poster for ui view,\n      // replace with actual src\n      stack.poster = input.posterSrc || input.poster;\n      // use identityId instead of avatar url\n      if ((input as StackPublishInput).identityId) {\n        stack.userIdentityId = (input as StackPublishInput).identityId;\n      }\n      stack.tags = input.tags;\n\n      // set this to be upgraded\n      stack.hlsMeta = JSON.stringify({\n        // ...stack.hlsMeta || {},\n        ...HLS_META_UPGRADE,\n      });\n\n      this.saveUpdatesToStore({ ...stack, clips: input.clips });\n\n      DEBUG_LOGS && console.log(`${PAGE} publishing stack...`, stack);\n\n      /**\n       * MVP-995 Stack Drafts\n       */\n      const createdStack = await this.stackApi.createStack(stack);\n      DEBUG_LOGS && console.log(`${PAGE} api.createStack res:`, createdStack);\n\n      // increment numStacks for each clip\n      if (!createdStack || !createdStack.stackId) {\n        throw new Error('Error creating stack - no ID?');\n      }\n      stack.shareUrl = createdStack.shareUrl;\n\n      this.mystackService.publishStack(stack);\n      this.stackService.addStack(stack); // could be updated to allow reducer to update based on mystack action, if the input was changed to stack array\n\n      /**\n       * Now watch for updates from the store...\n       */\n      this.stackService.subStacksUpdated(stack.projectId, stack.stackId);\n\n      // update analytics\n      const meMember =\n        (input as StackPublishInput).project && Array.isArray((input as StackPublishInput).project.members)\n          ? this.projectService.getRoleOfUserId((input as StackPublishInput).project, input.userId)\n          : '';\n      let role = ProjectRole.Public;\n      // there's gotta be a better way to do this?\n      if (meMember && meMember.role) {\n        switch (meMember.role) {\n          case PROJECT_MEMBER_ROLE.OWNER:\n            role = ProjectRole.Owner;\n            break;\n          case PROJECT_MEMBER_ROLE.EXECUTIVE:\n            role = ProjectRole.Executive;\n            break;\n          case PROJECT_MEMBER_ROLE.PRODUCER:\n            role = ProjectRole.Producer;\n            break;\n          case PROJECT_MEMBER_ROLE.CREW:\n            role = ProjectRole.Owner;\n            break;\n          default:\n            role = ProjectRole.Public;\n        }\n      }\n      this.analyticsService.stackPublished(stack.projectId, stack.stackId, role);\n      // increment numStacksPublished\n      this.userService.stackPublished();\n    } catch (error) {\n      this.sentryService.captureError(error);\n      throw error;\n    }\n\n    return stack;\n  }\n\n  /**\n   * save state of my stack to store to save for reload\n   */\n  saveUpdatesToStore(stack: Stack) {\n    this.mystackService.prePublishStack(stack);\n  }\n}\n","/**\n * Migrating to @app/stacks/shared/services\n * @format\n */\n\nimport { Injectable } from '@angular/core';\nimport { Observable, combineLatest, lastValueFrom } from 'rxjs';\nimport { map, first } from 'rxjs/operators';\nimport { Storage } from 'aws-amplify';\n\nimport { CoreLogicApiService } from '../api/core-logic-api.service';\nimport { EventsService } from './events.service';\nimport { Utils } from '@app/shared/utils';\n\nimport { Store } from '@ngrx/store';\nimport { State } from '@store/reducers';\nimport * as stackActions from '@store/actions/stacks.actions';\nimport { splitId as splitClipId } from '@store/selectors/clips.selectors';\nimport {\n  getStacksAll,\n  getStacksLoaded,\n  getStacksGroupNextToken,\n  StackGroup,\n  getProjectStacksGroup,\n} from '@store/selectors/stacks.selectors';\n// import { getClipEntities } from '@store/selectors/clips.selectors';\nimport { Clip } from '@app/shared/models/clip.model';\nimport { Stack, IOrderedClip } from '@app/shared/models/stack.model';\nimport { GraphQlParamsStacks, UpdateParam } from '../api/api-types';\nimport { StacksApiService } from '../api/stacks-api.service';\nimport { UpdateParamInt } from '../api/api-types';\nimport { UserService } from './user.service';\nimport { SentryService } from './sentry.service';\n\nconst DEBUG_LOGS = false;\n\nconst S3_STACK_POSTER_UPLOAD_PATH = 'uploads/images/';\n\nexport interface SaveStackPosterResponse {\n  link?: string; // if it needs to have auth\n  src: string; // what to save with the stack\n}\n\nconst PAGE = '[StackService]';\n/**\n * serves as an intermediary between the application store and the rest of the application\n * https://onehungrymind.com/handle-multiple-angular-2-models-ngrx-computed-observables/\n */\n@Injectable({\n  providedIn: 'root',\n})\nexport class StacksService {\n  /*\n   * ngrx Store connections\n   */\n  selectAllStacks$: Observable<Stack[]> = this.store.select(getStacksAll);\n  // selectStackPlay$: Observable<Stack> = this.store.select(fromStore.getSelectedStackPlay);\n  // selectStackEdit$: Observable<Stack> = this.store.select(fromStore.getSelectedStackEdit);\n  /** @deprecated selectLoaded$ required? */\n  selectLoaded$: Observable<string[]> = this.store.select(getStacksLoaded);\n\n  constructor(\n    private store: Store<State>,\n    private stacksApi: StacksApiService,\n    private userService: UserService,\n    private events: EventsService,\n    private coreApi: CoreLogicApiService,\n    private sentryService: SentryService\n  ) {}\n\n  /**\n   * Create a Stack UID\n   * @param projectId\n   * @param stackId\n   */\n  makeId(projectId: string, stackId: string): string {\n    return `${projectId}/${stackId}`;\n  }\n\n  getNextToken(group: string): Observable<string> {\n    return this.store.select(getStacksGroupNextToken(group));\n  }\n\n  // /**\n  //  * @deprecated - to be removed from project-detail once complete migration of project page to that page...\n  //  * Get Project Stacks\n  //  */\n  // getProjectStacksRecent(projectId: string): Observable<Stack[]> {\n  //   const recentStacks$ = this.selectAllStacks$.pipe(\n  //     map((stacks: Stack[]) => stacks.filter((stack) => stack && stack.projectId && stack.projectId === projectId)),\n  //     map((stacks: Stack[]) => stacks.sort(sortRecent)),\n  //   );\n  //   return this.getLoadedStacksOrUndefined(recentStacks$, projectId, StackGroup.Recent);\n  // }\n  // /**\n  //  * @deprecated - to be removed from project-detail once complete migration of project page to that page...\n  //  * Get Project Stacks\n  //  */\n  // getProjectStacksFeatured(projectId: string): Observable<Stack[]> {\n  //   const featuredStacks$ = this.selectAllStacks$.pipe(\n  //     map(\n  //       (stacks: Stack[]) =>\n  //         stacks.filter(\n  //           (stack) =>\n  //             stack &&\n  //             stack.projectId &&\n  //             stack.projectId === projectId &&\n  //             typeof stack.featured === 'number' &&\n  //             stack.featured > 0,\n  //         )\n  //     ),\n  //     map((stacks: Stack[]) => stacks.sort(sortFeatured)),\n  //   );\n  //   return this.getLoadedStacksOrUndefined(featuredStacks$, projectId, StackGroup.Featured);\n  // }\n\n  // /**\n  //  * @deprecated in favor of loadFilteredStacks\n  //  * Load Recent Stacks\n  //  */\n  // loadRecent() {\n  //   setTimeout(() => {\n  //     this.store.dispatch(stackActions.loadRecent({}));\n  //   }, REHYDRATE_DELAY);\n  // }\n\n  // /**\n  //  * @deprecated in favor of loadFilteredStacks\n  //  * Load Featured Stacks\n  //  */\n  // loadFeatured() {\n  //   setTimeout(() => {\n  //     this.store.dispatch(stackActions.loadFeatured({}));\n  //   }, REHYDRATE_DELAY);\n  // }\n\n  // /**\n  //  * @deprecated in favor of loadFilteredStacks\n  //  * Load Trending Stacks\n  //  */\n  // loadTrending() {\n  //   setTimeout(() => {\n  //     this.store.dispatch(stackActions.loadTrending({}));\n  //   }, REHYDRATE_DELAY);\n  // }\n\n  // /**\n  //  * @deprecated in favor of loadFilteredStacks\n  //  */\n  // loadStacks() {\n  //   // this.store.dispatch(stackActions.load()); // deprecated\n  //   this.loadRecent();\n  // }\n\n  loadFeaturedProjectStacks(projectId: string) {\n    this.store.dispatch(stackActions.loadProjectFeaturedStacks({ projectId }));\n  }\n  loadRecentProjectStacks(projectId: string) {\n    this.store.dispatch(stackActions.loadProjectRecentStacks({ projectId }));\n  }\n  loadProjectStacks(projectId: string) {\n    this.store.dispatch(stackActions.loadProjectRecentStacks({ projectId }));\n  }\n\n  // /**\n  //  * @deprecated in favor of loadMoreFilteredStacks\n  //  */\n  // loadMoreFeatured(startIndex = 0, limit = 20) {\n  //   console.log(`${PAGE} loadMoreFeatured...`);\n  //   this.store.dispatch(stackActions.loadMoreFeatured({}));\n  // }\n\n  // loadMoreRecent(startIndex = 0, limit = 20) {\n  //   console.log(`${PAGE} loadMoreRecent...`);\n  //   this.store.dispatch(stackActions.loadMoreRecent({}));\n  // }\n\n  // loadMoreTrending() {\n  //   console.log(`${PAGE} loadMoreTrending...`);\n  //   this.store.dispatch(stackActions.loadMoreTrending({}));\n  // }\n\n  loadMoreProjectStacks(projectId: string, group: StackGroup) {\n    console.log(`${PAGE} loadMoreProjectStacks...[${group}]`);\n\n    switch (group) {\n      case StackGroup.Featured:\n        this.store.dispatch(stackActions.loadMoreProjectFeaturedStacks({ projectId }));\n        break;\n      case StackGroup.Recent:\n        this.store.dispatch(stackActions.loadMoreProjectRecentStacks({ projectId }));\n        break;\n      default:\n        console.log(`${PAGE} loadMoreProjectStacks UNHANDLED group: '${group}'`);\n    }\n  }\n\n  /** used by publishService */\n  addStack(stack: Stack): void {\n    this.store.dispatch(stackActions.add({ stacks: [stack] }));\n  }\n\n  /**\n   * Query the API for SearchText\n   * NOTE: searchFields[] in stack.model.ts\n   * @note unused\n   */\n  searchStacks(term: string, projectId: string = '') {\n    console.log(`${PAGE} searchStacks - TODO!`, { term, projectId });\n    // this.store.dispatch(stackActions.search()); // createEffect -> API\n  }\n  /** @note unused */\n  addStackTags(projectId: string, stackId: string, tags: string[]) {\n    console.log(`${PAGE} TODO: API: addTags: %o`, { tags, projectId, stackId });\n  }\n  /** @note unused */\n  removeStackTags(projectId: string, stackId: string, tags: string[]) {\n    console.log(`${PAGE} TODO: API: removeTags: %o`, { tags, projectId, stackId });\n  }\n  /** @note unused */\n  addStacks(stacks: Stack[]): void {\n    this.store.dispatch(stackActions.add({ stacks }));\n  }\n\n  /** @note unused */\n  getAvatarUrl(stack: Stack) {\n    return this.userService.getUserAvatarUrl(stack.userId);\n  }\n\n  /**\n   * Save an image as the Stack Poster\n   * @param userId\n   * @param posterId\n   * @param imageBlob\n   * @param contentType\n   */\n  saveStackPoster(\n    userId: string,\n    posterId: string,\n    imageBlob: Blob,\n    contentType: string = 'image/jpeg'\n  ): Promise<SaveStackPosterResponse> {\n    console.log(`${PAGE} saveStackPoster: ${userId}_-_${posterId}`);\n\n    const ext = Utils.mimeToExtension(contentType);\n    const posterName = `${userId}_-_${posterId}.${ext}`;\n\n    return Storage.put(S3_STACK_POSTER_UPLOAD_PATH + posterName, imageBlob, {\n      level: 'public',\n      contentType,\n      // progressCallback(progress) {\n      //   console.log(`Uploaded: ${progress.loaded}/${progress.total}`);\n      // }\n    }).then((res) => {\n      console.log(`${PAGE} saveStackPoster res:`, res);\n      if (res && res['key']) {\n        const config = Storage.vault.configure();\n        const bucket = config && config.AWSS3 && config.AWSS3.bucket ? config.AWSS3.bucket : '';\n        const key: string = res['key'];\n        const src = `${bucket}/public/${key}`;\n        console.log(`${PAGE} saveStackPoster src:`, src);\n\n        return Storage.get(key).then((link) => ({\n          link: link as string,\n          src, // TODO: send this with final resting place...\n        }));\n      }\n      return {\n        link: '',\n        src: '',\n      };\n    });\n  }\n\n  /**\n   * @v2\n   * RealTimeData GraphQL Subscription MVP-1030\n   * Just an observable, right?\n   * moving to core-logic as root of http gql\n   */\n  subStacksUpdated$(projectId = '', stackId = '', userId = '') {\n    console.log('TODO: update from v1 to v2 and shared CoreLib sub..', { projectId, stackId, userId });\n    return this.coreApi.watchAllStacksData$;\n  }\n\n  /**\n   * @v1\n   * clipUpdate GraphQL Subscription\n   */\n  subStacksUpdated(projectId: string, stackId = '', userId = '') {\n    if (userId) {\n      console.warn('UserId was provided but we are not using it... fyi!');\n    }\n    const params: GraphQlParamsStacks = {\n      projectId,\n    };\n    if (stackId) {\n      params.stackId = stackId;\n    }\n    // not currently using userId on this GQL subscription in the API\n    // if (userId) {  params.userId = userId; }\n\n    // keeping the sub variable local so there can be multiple open subs\n    const sub = this.stacksApi.subscribeStacksUpdated(params).subscribe({\n      next: (res) => {\n        // DEBUG_LOGS &&\n        console.log(`${PAGE} stacksUpdatedSubscription next:`, res);\n        if (!res || !res.stackId || !res.projectId) {\n          throw new Error('Missing stack id in subscription?');\n        }\n        const updates: UpdateParam[] = [];\n        let hlsMeta, hlsSrc;\n        if (res.hlsMeta) {\n          try {\n            hlsMeta = typeof res.hlsMeta === 'string' ? JSON.parse(res.hlsMeta) : res.hlsMeta;\n            updates.push({ prop: 'hlsMeta', value: hlsMeta });\n          } catch (error) {\n            console.log(`Caught error during JSON.parse(res.hlsMeta):`, {\n              hlsMeta: res.hlsMeta,\n              error: error.message || error,\n            });\n          }\n        }\n        if (res.hlsSrc) {\n          hlsSrc = res.hlsSrc;\n          updates.push({ prop: 'hlsSrc', value: hlsSrc });\n        }\n        if (res.hero) {\n          updates.push({ prop: 'hero', value: res.hero });\n        }\n        if (res.duration) {\n          updates.push({ prop: 'duration', value: res.duration });\n        }\n\n        // take the updates and flow to store...\n        this.store.dispatch(\n          stackActions.update({\n            stack: res,\n            updates,\n          })\n        );\n\n        if (hlsSrc && hlsSrc.length > 0) {\n          // when complete, this.unsubClipsUpdated()\n          sub.unsubscribe();\n        }\n      },\n      error: (err) => {\n        console.warn(`${PAGE} stacksUpdatedSubscription ERROR:`, err);\n        this.sentryService.captureError(err);\n\n        if (projectId && stackId) {\n          // this.store.dispatch(clipActions.updateClipTranscoding({\n          //   projectId,\n          //   id,\n          //   updates: {\n          //     hlsMeta: {\n          //       errorMessage:  err.message || err.errorMessage || err\n          //     },\n          //   }\n          // }));\n        }\n        sub.unsubscribe();\n      },\n      complete: () => {\n        // DEBUG_LOGS &&\n        console.log(`${PAGE} stacksUpdatedSubscription => Complete`);\n      },\n    });\n  }\n  //end GraphQL Subscription\n\n  /**\n   * take clipIds: string[] create IOrderedClip[] playlist\n   * @returns IOrderedClip[]\n   *\n   * Currently UNUSED\n   */\n  createPlaylistFromClipIds(ids: string[]): IOrderedClip[] {\n    if (!Array.isArray(ids)) {\n      throw new Error('Missing ids?');\n    }\n    const orderedClips = [];\n\n    ids.forEach((id, index) => {\n      const clipId = splitClipId(id);\n      // quick error checking\n      if (!clipId || !clipId.id || !clipId.projectId) {\n        console.warn(`${PAGE} createPlaylistFromClipIds Missing ClipId ! `, { id, index, ids });\n        this.sentryService.captureMessage(`createPlaylistFromClipIds Missing ClipId: '${id}'`);\n      } else {\n        orderedClips.push({\n          ...splitClipId(id),\n          order: index,\n        });\n      }\n    });\n    return orderedClips;\n  }\n\n  /**\n   * UNUSED\n   */\n  createPlaylistFromClips(clips: Clip[]) {\n    if (clips.filter((item) => !item.projectId || !item.id)) {\n      throw new Error('Missing a clip.projectId or clip.id !');\n    }\n    return clips.map((clip, index) => ({\n      projectId: clip.projectId,\n      id: clip.id,\n      order: index,\n    }));\n  }\n\n  /**\n   * Update a Stack - supply the Stack and an array of UpdateParam{ prop: string, value: any }\n   * @param stack\n   * @param updates\n   */\n  async updateStack(stack: Stack, updates: UpdateParam[]): Promise<Stack> {\n    if (!stack || !stack.projectId || !stack.stackId) {\n      return Promise.reject('Missing required arguments: stack ids');\n    }\n    try {\n      // https://stackoverflow.com/questions/34190375/how-can-i-await-on-an-rx-observable\n      const userId = await lastValueFrom(this.userService.userId$.pipe(first()));\n      updates.push({ prop: 'updatedBy', value: userId });\n\n      const updatedStack = await this.stacksApi.updateStack(stack, updates);\n      DEBUG_LOGS && console.log(`${PAGE} updatedStack`, { updatedStack, updates });\n\n      this.store.dispatch(stackActions.update({ stack: updatedStack, updates }));\n      return updatedStack;\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  }\n\n  /**\n   * delete Stack and remove from store\n   * @param stack\n   */\n  deleteStack(stack: Stack): Promise<string> {\n    DEBUG_LOGS && console.log(`${PAGE} deleteStack`, stack);\n    return this.stacksApi\n      .deleteStack(stack.projectId, stack.stackId)\n      .then((res) => {\n        DEBUG_LOGS && console.log(`${PAGE} deleteStack res:`, res);\n        this.store.dispatch(stackActions.deleteStack({ stack }));\n        return 'Stack deleted';\n      })\n      .catch((err) => {\n        console.error(`${PAGE} deleteStack err:`, err);\n        throw err;\n      });\n  }\n\n  /** appears unused... */\n  private incrementStackAdmin(projectId: string, stackId: string, updates: UpdateParamInt[]): Promise<Partial<Stack>> {\n    return this.stacksApi.incrementAdmin(projectId, stackId, updates).then((stack) => {\n      this.store.dispatch(stackActions.update({ stack, updates }));\n      return stack;\n    });\n  }\n  /** appears unused... */\n  private incrementStackPublic(projectId: string, stackId: string, updates: UpdateParamInt[]): Promise<Partial<Stack>> {\n    return this.stacksApi.incrementPublic(projectId, stackId, updates).then((stack) => {\n      this.store.dispatch(stackActions.update({ stack, updates }));\n      return stack;\n    });\n  }\n\n  /**\n   * encapuslate stack observables with the information if they have been loaded or not.\n   * if they're still being loaded, the observable will return `undefined`. otherwise `[…]`.\n   * this helps to distinguish if there are no stacks to display or if there are no stacks yet, because they're still being requested.\n   */\n  private getLoadedStacksOrUndefined(\n    stacks$: Observable<Stack[]>,\n    projectId: string,\n    group: StackGroup\n  ): Observable<Stack[]> {\n    return combineLatest([stacks$, this.selectLoaded$]).pipe(\n      map(([stacks, loadedStacks]) =>\n        Array.isArray(loadedStacks) && loadedStacks.indexOf(getProjectStacksGroup(projectId, group)) !== -1\n          ? stacks\n          : undefined\n      )\n    );\n  }\n}\n","/**\n * Youtube API Service\n * based on: https://piratesofjs.wordpress.com/2017/02/13/ionic-2-app-tutorial-a-simple-youtube-player/\n *\n * ref:\n * https://stackoverflow.com/questions/39022009/youtube-api-get-tags-for-all-videos-with-playlist-query\n *\n * @format\n */\n\nimport { Injectable } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { lastValueFrom } from 'rxjs';\nimport { MAX_DESC_LENGTH_CLIPS } from '@app/shared/models/clip.model';\nimport { Utils } from '@app/shared/utils';\nimport { environment } from 'src/environments/environment';\n\nconst DEBUG_LOGS = false;\n\nconst PAGE = '[YoutubeService]';\n\n@Injectable({\n  providedIn: 'root',\n})\nexport class YoutubeService {\n  googleApiKey: string = '';\n  maxResults = 5;\n  queryUrl = 'https://www.googleapis.com/youtube/v3/search?part=id,snippet&q=';\n\n  constructor(private http: HttpClient) {\n    if (environment.youtubeApiKey) {\n      this.googleApiKey = environment.youtubeApiKey;\n      // console.log(`${PAGE} youtubeApiKey:`, this.googleApiKey);\n    } else {\n      console.warn(`${PAGE} env NO API KEY!!`, environment);\n    }\n  }\n\n  /**\n   * helper getters for videoMetadata\n   */\n\n  getMetadataTitle(data) {\n    return data && data.snippet && data.snippet.title ? data.snippet.title : '';\n  }\n  getMetadataDescription(data) {\n    const o = Utils.tryParseJSON(data);\n    DEBUG_LOGS && console.log('get desc', o);\n    return o && o.snippet && o.snippet.description ? o.snippet.description.substring(0, MAX_DESC_LENGTH_CLIPS) : '';\n  }\n\n  getMetadataPublishDate(data) {\n    return data && data.snippet && data.snippet.publishedAt ? data.snippet.publishedAt : '';\n  }\n\n  /**\n   * The length of the video.\n   * https://developers.google.com/youtube/v3/docs/videos#properties\n   * The property value is an ISO 8601 duration.\n   * For example, for a video that is at least one minute long and less than one hour long,\n   * the duration is in the format PT#M#S, in which the letters PT indicate that the value\n   * specifies a period of time, and the letters M and S refer to length in minutes and seconds, respectively.\n   * The # characters preceding the M and S letters are both integers that specify the\n   * number of minutes (or seconds) of the video.\n   * For example, a value of PT15M33S indicates that the video is 15 minutes and 33 seconds long.\n   *\n   * If the video is at least one hour long, the duration is in the format PT#H#M#S,\n   * in which the # preceding the letter H specifies the length of the video in hours and\n   * all of the other details are the same as described above.\n   * If the video is at least one day long, the letters P and T are separated,\n   * and the value's format is P#DT#H#M#S. Please refer to the ISO 8601 specification for complete details.\n   */\n  getMetadataDuration(data) {\n    if (data && data.contentDetails && data.contentDetails.duration) {\n      const dur = this.parseISO8601Duration(data.contentDetails.duration);\n      const padStart = (val) => ('0' + val).slice(-2);\n      const hours = dur.days * 24 + dur.hours;\n      return `${padStart(hours)}:${padStart(dur.minutes)}:${padStart(dur.seconds)}`;\n    }\n    return '';\n  }\n  // example with all options: https://stackoverflow.com/questions/14934089/convert-iso-8601-duration-with-javascript\n  // private iso8601DurationRegex = /(-)?P(?:([.,\\d]+)Y)?(?:([.,\\d]+)M)?(?:([.,\\d]+)W)?(?:([.,\\d]+)D)?T(?:([.,\\d]+)H)?(?:([.,\\d]+)M)?(?:([.,\\d]+)S)?/;\n  // eslint-disable-next-line @typescript-eslint/member-ordering\n  private iso8601DurationRegex = /P(?:([.,\\d]+)D)?(?:T(?:([.,\\d]+)H)?(?:([.,\\d]+)M)?(?:([.,\\d]+)S)?)?/; // simpler\n  parseISO8601Duration(iso8601Duration) {\n    const matches = iso8601Duration.match(this.iso8601DurationRegex);\n    return {\n      days: matches[1] === undefined ? 0 : matches[1],\n      hours: matches[2] === undefined ? 0 : matches[2],\n      minutes: matches[3] === undefined ? 0 : matches[3],\n      seconds: matches[4] === undefined ? 0 : matches[4],\n    };\n  }\n\n  /**\n   * get the video metadata for an id\n   * @param id \n   * retrieves information about a group of videos. \n   * The id parameter value is a comma-separated list of YouTube video IDs. \n   * You might issue a request like this to retrieve additional information about the items in a playlist or the results of a search query.\n   * https://developers.google.com/youtube/v3/docs/videos/list\n   \n   * \n   * https://www.googleapis.com/youtube/v3/videos?key={API-key}&fields=items(snippet(title,description,tags))&part=snippet&id={video_id}\n   */\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  getVideoMetadata(ids: string[]): Promise<any[]> {\n    if (!ids || ids.length < 1) {\n      return Promise.reject(`No Youtube ID provided.`);\n    }\n    if (!this.googleApiKey) {\n      console.warn(`${PAGE} env NO API KEY!!`, environment);\n      return Promise.reject(`Config error.`);\n    }\n    // comma-separated list of YouTube video IDs ex: Ks-_Mh1QhMc,c0KYU2j0TM4,eIho2S0ZahI\n    const csvIds = ids.join(',');\n    /* https://developers.google.com/youtube/v3/docs/videos\n      Quota Cost\n      2 - The snippet object contains basic details about the video, \n          snippet = {title, description, thumbnails, publishedAt, channelId, channelTitle, tags, category\n\n      2 - The contentDetails object contains information about the video content, \n          contentDetails = { duration, definition (hd or sd),  caption (true/false), licensedContent, regionRestriction, contentRating\n\n      2 - The status object contains information about the video's uploading, processing, and privacy statuses\n          status = { embeddable\n\n      2 - The statistics object contains statistics about the video\n          statistics = { viewCount, likeCount, dislikeCount, favoriteCount, commentCount\n\n      2 - The recordingDetails object encapsulates information about the location, date and address where the video was recorded\n          recordingDetails = { location, recordingDate, \n\n        &fields=items(snippet(title,description,tags))\n    */\n    const endpoint =\n      `https://www.googleapis.com/youtube/v3/videos` +\n      `?key=${this.googleApiKey}` +\n      // don't need the thumbnails from snippet, shrink response (removed: thumbnails,liveBroadcastContent,localized)\n      //  + `&fields=items(id,snippet,contentDetails,status(embeddable),statistics,recordingDetails(location, recordingDate))`\n      `&fields=items(id,snippet(publishedAt,channelId,title,description,channelTitle,categoryId,defaultAudioLanguage),contentDetails,status(embeddable),statistics,recordingDetails(location, recordingDate))` +\n      `&part=id,snippet,contentDetails,status,statistics,recordingDetails` +\n      `&id=${csvIds}`;\n    // this.http.get(endpoint, { observe: 'response' }) // get full response (with headers)\n    return lastValueFrom(this.http.get(endpoint))\n      .then((data) => {\n        // success path\n        DEBUG_LOGS && console.log(`${PAGE} getVideoMetadata:`, data);\n        // handle multiple & error checking...\n        return data && data['items'] ? data['items'] : [];\n        // return items.map(item => {\n        //   return item.snippet || { error: \"No Youtube snippet found\" };\n        // });\n      })\n      .catch((error) => {\n        console.warn(`${PAGE} getVideoMetadata error:`, error);\n        return [{ error: 'Caught Error' }];\n      });\n    // .subscribe(\n    //   (data: any) => {  // success path\n    //     console.log(`${PAGE} getVideoMetadata:`,data);\n    //   },\n    //   error => { // error path\n    //     console.log(`${PAGE} getVideoMetadata error:`,error);\n    //   }\n    // );\n  }\n\n  /** THUMBNAILS\n   * if need api: https://developers.google.com/youtube/v3/docs/thumbnails\n   */\n\n  /** \n   * getThumbnail via http url by videoId\n   * @param videoId \n   * @param res \n   * \n      The default thumbnail for a video – or a resource that refers to a video, such as a playlist item or search result \n      – is 120px wide and 90px tall. The default thumbnail for a channel is 88px wide and 88px tall.\n      https://img.youtube.com/vi/5NQUyrlHYB0/default.jpg\n      A higher resolution version of the thumbnail image. For a video (or a resource that refers to a video), \n      this image is 320px wide and 180px tall. For a channel, this image is 240px wide and 240px tall.\n      https://img.youtube.com/vi/5NQUyrlHYB0/mqdefault.jpg\n      A high resolution version of the thumbnail image. For a video (or a resource that refers to a video), \n      this image is 480px wide and 360px tall. For a channel, this image is 800px wide and 800px tall.\n      https://img.youtube.com/vi/5NQUyrlHYB0/hqdefault.jpg\n      An even higher resolution version of the thumbnail image than the high resolution image. \n      This image is available for some videos and other resources that refer to videos, like playlist items or search results. \n      This image is 640px wide and 480px tall.\n      https://img.youtube.com/vi/5NQUyrlHYB0/sddefault.jpg\n      The highest resolution version of the thumbnail image. \n      This image size is available for some videos and other resources that refer to videos, like playlist items or search results. \n      This image is 1280px wide and 720px tall.\n      https://img.youtube.com/vi/5NQUyrlHYB0/maxresdefault.jpg\n   */\n  getThumbnail(videoId: string, res: string = 'default'): { url: string; width: number; height: number } {\n    const rootUrl = 'https://i.ytimg.com/vi/'; // saw this url in api video list response 26 oct 18\n    switch (res) {\n      case 'low': // 320px wide and 180px tall. For a channel, this image is 240px wide and 240px tall.\n        return {\n          width: 320,\n          height: 180,\n          url: `${rootUrl}${videoId}/mqdefault.jpg`,\n        };\n      case 'med': // 480px wide and 360px tall. For a channel, this image is 800px wide and 800px tall.\n        return {\n          width: 480,\n          height: 360,\n          url: `${rootUrl}${videoId}/hqdefault.jpg`,\n        };\n      case 'high': // 640px wide and 480px tall.\n        return {\n          width: 640,\n          height: 480,\n          url: `${rootUrl}${videoId}/sddefault.jpg`,\n        };\n      case 'maxres': // 1280px wide and 720px tall.\n        return {\n          width: 1280,\n          height: 720,\n          url: `${rootUrl}${videoId}/maxresdefault.jpg`,\n        };\n      case 'default': //120px wide and 90px tall\n      default:\n        return {\n          width: 120,\n          height: 90,\n          url: `${rootUrl}${videoId}/default.jpg`,\n        };\n    }\n  }\n\n  // private YOUTUBE_REGEX = /^.*(youtu\\.be\\/|vi?\\/|u\\/\\w\\/|embed\\/|\\?vi?=|\\&vi?=)([^#\\&\\?]*).*/; // id=parsed[2]\n  // including the case where it's just an id string\n  // eslint-disable-next-line @typescript-eslint/member-ordering, @typescript-eslint/naming-convention\n  private YOUTUBE_REGEX = /(^.*(youtu\\.be\\/|vi?\\/|u\\/\\w\\/|embed\\/|\\?vi?=|\\&vi?=)([^#\\&\\?]*).*)|([^#\\&\\?]*)/; // id=parsed[3] || parsed[4]\n  /**\n   * get the YouTube ID from a url string\n   * @param url\n   * https://stackoverflow.com/questions/3452546/how-do-i-get-the-youtube-video-id-from-a-url/27728417#27728417\n   * or, /(?:(?:\\?|&)v=|embed\\/|v\\/|youtu\\.be\\/)((?!videoseries)[a-zA-Z0-9_]*)/g\n   */\n  getYoutubeVideoIdFromUrl(url: string) {\n    const parsed = url.match(this.YOUTUBE_REGEX);\n    if (parsed && (parsed[3] || parsed[4])) {\n      // console.log(parsed[3] || parsed[4]);\n      return parsed[3] || parsed[4];\n    } else {\n      console.log(`${PAGE} youtube id not found in url:`, url, parsed);\n      return '';\n    }\n  }\n\n  // private YOUTUBE_PLAYLIST_REGEX = /(?:youtube\\.com.*(?:\\?|&)(?:list)=)((?!videoseries)[a-zA-Z0-9_]*)/g;\n  // https://stackoverflow.com/questions/49535029/regex-get-youtube-playlist-id\n  // eslint-disable-next-line @typescript-eslint/member-ordering, @typescript-eslint/naming-convention\n  private YOUTUBE_PLAYLIST_REGEX = /[&?]list=([^&]+)/i;\n  /** (not tested yet)\n   * get the YouTube Playlist ID from a url string\n   * @param url\n   * https://stackoverflow.com/questions/32295157/regex-to-extract-both-video-id-or-playlist-id-from-youtube-url\n   * only grabs list:    /(?:(?:\\?|&)list=)((?!videoseries)[a-zA-Z0-9_]*)/g\n   * only youtube lists: /(?:youtube\\.com.*(?:\\?|&)(?:list)=)((?!videoseries)[a-zA-Z0-9_]*)/g\n   */\n  getYoutubePlaylistIdFromUrl(url: string) {\n    const parsed = url.match(this.YOUTUBE_PLAYLIST_REGEX);\n    if (parsed && parsed[1]) {\n      // console.log(parsed);\n      return parsed[1];\n    } else {\n      console.log(`${PAGE} youtube playlist id not found in url:`, url, parsed);\n      return '';\n    }\n  }\n\n  testGetYoutubeVideoIdFromUrl() {\n    const testUrls = [\n      'http://www.youtube.com/watch?v=0zM3nApSvMg&feature=feedrec_grec_index',\n      'http://www.youtube.com/user/IngridMichaelsonVEVO#p/a/u/1/QdK8U-VIH_o',\n      'http://www.youtube.com/v/0zM3nApSvMg?fs=1&amp;hl=en_US&amp;rel=0',\n      'http://www.youtube.com/watch?v=0zM3nApSvMg#t=0m10s',\n      'http://www.youtube.com/embed/0zM3nApSvMg?rel=0',\n      'http://www.youtube.com/watch?v=0zM3nApSvMg',\n      'http://youtu.be/0zM3nApSvMg',\n      '//www.youtube-nocookie.com/embed/up_lNV-yoK4?rel=0',\n      'http://www.youtube.com/user/Scobleizer#p/u/1/1p3vcRhsYGo',\n      'http://www.youtube.com/watch?v=cKZDdG9FTKY&feature=channel',\n      'http://www.youtube.com/watch?v=yZ-K7nCVnBI&playnext_from=TL&videos=osPknwzXEas&feature=sub',\n      'http://www.youtube.com/ytscreeningroom?v=NRHVzbJVx8I',\n      'http://www.youtube.com/user/SilkRoadTheatre#p/a/u/2/6dwqZw0j_jY',\n      'http://youtu.be/6dwqZw0j_jY',\n      'http://www.youtube.com/watch?v=6dwqZw0j_jY&feature=youtu.be',\n      'http://youtu.be/afa-5HQHiAs',\n      'http://www.youtube.com/user/Scobleizer#p/u/1/1p3vcRhsYGo?rel=0',\n      'http://www.youtube.com/watch?v=cKZDdG9FTKY&feature=channel',\n      'http://www.youtube.com/watch?v=yZ-K7nCVnBI&playnext_from=TL&videos=osPknwzXEas&feature=sub',\n      'http://www.youtube.com/ytscreeningroom?v=NRHVzbJVx8I',\n      'http://www.youtube.com/embed/nas1rJpm7wY?rel=0',\n      'http://www.youtube.com/watch?v=peFZbP64dsU',\n      'http://youtube.com/v/dQw4w9WgXcQ?feature=youtube_gdata_player',\n      'http://youtube.com/vi/dQw4w9WgXcQ?feature=youtube_gdata_player',\n      'http://youtube.com/?v=dQw4w9WgXcQ&feature=youtube_gdata_player',\n      'http://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=youtube_gdata_player',\n      'http://youtube.com/?vi=dQw4w9WgXcQ&feature=youtube_gdata_player',\n      'http://youtube.com/watch?v=dQw4w9WgXcQ&feature=youtube_gdata_player',\n      'http://youtube.com/watch?vi=dQw4w9WgXcQ&feature=youtube_gdata_player',\n      'http://youtu.be/dQw4w9WgXcQ?feature=youtube_gdata_player',\n      'dQw4w9WgXcQ',\n    ];\n\n    let failures = 0;\n    testUrls.forEach((url) => {\n      const res = this.getYoutubeVideoIdFromUrl(url);\n      if (!res) {\n        failures++;\n      }\n    });\n    if (failures) {\n      console.error(`testGetYoutubeIdFromUrl result: ${failures} failures`);\n    }\n  }\n}\n"],"x_google_ignoreList":[]}