<commit_before>
import EntityManager from "idb-entity/es2015/EntityManager"
import createPrivate from "namespace-proxy"
import Account from "../../model/Account"
import Channel from "../../model/Channel"
import Playlist from "../../model/Playlist"
import Video from "../../model/Video"
import Subscription from "../../model/Subscription"
import Database from "../storage/Database"
import ClientFactory from "../youtube-api/ClientFactory"

const PRIVATE = createPrivate()

/**
 * Utility for fetching and updating the list of subscribed channels of a
 * single user's Google account.
 */
export default class SubscriptionsFetcher {
  /**
   * Initializes the subscription fetcher.
   *
   * @param database The current database connection provider.
   * @param clientFactory YouTube API client factory.
   */
  constructor(database: Database, clientFactory: ClientFactory) {
    /**
     * The current database connection provider.
     *
     * @type {Database}
     */
    PRIVATE(this).database = database

    /**
     * YouTube API client factory.
     *
     * @type {ClientFactory}
     */
    PRIVATE(this).clientFactory = clientFactory
  }

  async updateSubscriptions(account: Account): void {
    if (!account.channelId) {
      throw new Error("Cannot read the subscription of a foreign account, " +
          "because the channel ID is not known")
    }

    let allSubscriptions = await this[PRIVATE.fetchCurrentSubscriptions](
      account
    )

    let entityManager = PRIVATE(this).database.createEntityManager()
    entityManager.runTransaction(async () => {
      let knownSubscriptions = await this[PRIVATE.fetchSubscriptions](
        entityManager,
        account
      )

      for (let [subscription, channel] of allSubscriptions) {
        // save new subscriptions
        let knownSubscription = knownSubscriptions.get(channel.id)
        if (!knownSubscription) {
          knownSubscription = await entityManager.persist(subscription)
        }

        let knownChannel = await entityManager.find(Channel, channel.id)
        if (!knownChannel) {
          knownChannel = await entityManager.persist(channel)
        }
        let playlist = await entityManager.find(
            Playlist,
            knownChannel.uploadsPlaylistId
        )
        if (!playlist) {
          playlist = await this[PRIVATE.fetchUploadsPlaylist](channel)
          playlist = await entityManager.persist(playlist)
        }

        // update existing subscriptions
        if (!knownChannel.accountIds.includes(account.id)) {
          knownChannel.accountIds.push(account.id)
        }
        if (!playlist.accountIds.includes(account.id)) {
          playlist.accountIds.push(account.id)
          let videos = await entityManager.query(Video, {
            channelId: knownChannel.id
          })
          for (let video of videos) {
            if (!video.accountIds.includes(account.id)) {
              video.accountIds.push(account.id)
            }
          }
        }

        knownSubscriptions.delete(channel.id)
      }

      // delete subscriptions that were removed from the account
      for (let [channelId, subscription] of knownSubscriptions) {
        let channel = await entityManager.find(Channel, channelId)
        channel.accountIds = channel.accountIds.filter(
          otherAccountId => otherAccountId !== account.id
        )
        let shouldDeleteChannel =
            !channel.accountIds.length &&
            !channel.incognitoSubscriptionIds.length
        if (shouldDeleteChannel) {
          entityManager.remove(Channel, channel.id)
        }
        let playlist = await entityManager.find(
            Playlist,
            channel.uploadsPlaylistId
        )
        playlist.accountIds = playlist.accountIds.filter(
          otherAccountId => otherAccountId !== account.id
        )
        let shouldDeletePlaylist =
            !playlist.accountIds.length &&
            !playlist.incognitoSubscriptionIds.length
        if (shouldDeletePlaylist) {
          entityManager.remove(Playlist, playlist.id)
        }
        let videos = await entityManager.query(Video, {
          channelId: channelId
        })
        for (let video of videos) {
          video.accountIds = video.accountIds.filter(
            otherAccountId => otherAccountId !== account.id
          )
          let shouldDeleteVideo =
              !video.accountIds.length &&
              !video.incognitoSubscriptionIds.length
          if (shouldDeleteVideo) {
            entityManager.remove(Video, video.id)
          }
        }
      }
    })
  }

  /**
   * Fetches the uploads playlist entity from the YouTube API for the specified
   * channel.
   *
   * @param channel The YouTube channel for which the uploads playlist should
   *        be fetched.<commit_msg>refactoring, fixed deleting an unsubscribed subscription<commit_after>
import EntityManager from "idb-entity/es2015/EntityManager"
import createPrivate from "namespace-proxy"
import Account from "../../model/Account"
import Channel from "../../model/Channel"
import Playlist from "../../model/Playlist"
import Video from "../../model/Video"
import Subscription from "../../model/Subscription"
import Database from "../storage/Database"
import ClientFactory from "../youtube-api/ClientFactory"

const PRIVATE = createPrivate()

/**
 * Utility for fetching and updating the list of subscribed channels of a
 * single user's Google account.
 */
export default class SubscriptionsFetcher {
  /**
   * Initializes the subscription fetcher.
   *
   * @param database The current database connection provider.
   * @param clientFactory YouTube API client factory.
   */
  constructor(database: Database, clientFactory: ClientFactory) {
    /**
     * The current database connection provider.
     *
     * @type {Database}
     */
    PRIVATE(this).database = database

    /**
     * YouTube API client factory.
     *
     * @type {ClientFactory}
     */
    PRIVATE(this).clientFactory = clientFactory
  }

  async updateSubscriptions(account: Account): void {
    if (!account.channelId) {
      throw new Error("Cannot read the subscription of a foreign account, " +
          "because the channel ID is not known")
    }

    let allSubscriptions = await this[PRIVATE.fetchCurrentSubscriptions](
      account
    )

    let entityManager = PRIVATE(this).database.createEntityManager()
    entityManager.runTransaction(async () => {
      let knownSubscriptions = await this[PRIVATE.fetchSubscriptions](
        entityManager,
        account
      )

      for (let [subscription, channel] of allSubscriptions) {
        // save new subscriptions
        let knownSubscription = knownSubscriptions.get(channel.id)
        if (!knownSubscription) {
          knownSubscription = await entityManager.persist(subscription)
        }

        let knownChannel = await entityManager.find(Channel, channel.id)
        if (!knownChannel) {
          knownChannel = await entityManager.persist(channel)
        }
        let playlist = await entityManager.find(
            Playlist,
            knownChannel.uploadsPlaylistId
        )
        if (!playlist) {
          playlist = await this[PRIVATE.fetchUploadsPlaylist](channel)
          playlist = await entityManager.persist(playlist)
        }

        // update existing subscriptions
        if (!knownChannel.accountIds.includes(account.id)) {
          knownChannel.accountIds.push(account.id)
        }
        if (!playlist.accountIds.includes(account.id)) {
          playlist.accountIds.push(account.id)
          let videos = await entityManager.query(Video, {
            channelId: knownChannel.id
          })
          for (let video of videos) {
            if (!video.accountIds.includes(account.id)) {
              video.accountIds.push(account.id)
            }
          }
        }

        knownSubscriptions.delete(channel.id)
      }

      // delete subscriptions that were removed from the account
      for (let [channelId, subscription] of knownSubscriptions) {
        await this[PRIVATE.deleteUnsubsribedSubscription](
          entityManager,
          channelId,
          subscription
        )
      }
    })
  }

  /**
   * Deletes the provided subscription, which the user is no longer subscribed
   * to.
   *
   * The method also updates the account IDs of the subscribed channel, uploads
   * playlist and videos. If any of those will no longer be subscribed by any
   * account or incognito subscription, they will be deleted.
   *
   * @param entityManager The current entity manager in a transaction used to
   *        update the subscriptions.
   * @param channelId The ID of the channel that has been unsubscribed
   * @param subscriptions The subscription that should be deleted.
   */
  async [PRIVATE.deleteUnsubsribedSubscription](
    entityManager: EntityManager,
    channelId: string,
    subscriptions: Subscription
  ): void {
    // update and delete (if necessary) the channel
    let channel = await entityManager.find(Channel, channelId)
    channel.accountIds = channel.accountIds.filter(
      otherAccountId => otherAccountId !== account.id
    )
    let shouldDeleteChannel =
        !channel.accountIds.length &&
        !channel.incognitoSubscriptionIds.length
    if (shouldDeleteChannel) {
      entityManager.remove(Channel, channel.id)
    }

    // update and delete (if necessary) the playlist
    let playlist = await entityManager.find(
      Playlist,
      channel.uploadsPlaylistId
    )
    playlist.accountIds = playlist.accountIds.filter(
      otherAccountId => otherAccountId !== account.id
    )
    let shouldDeletePlaylist =
        !playlist.accountIds.length &&
        !playlist.incognitoSubscriptionIds.length
    if (shouldDeletePlaylist) {
      entityManager.remove(Playlist, playlist.id)
    }

    // updated and delete (if necessary) the videos
    let videos = await entityManager.query(Video, {
      channelId: channelId
    })
    for (let video of videos) {
      video.accountIds = video.accountIds.filter(
        otherAccountId => otherAccountId !== account.id
      )
      let shouldDeleteVideo =
          !video.accountIds.length &&
          !video.incognitoSubscriptionIds.length
      if (shouldDeleteVideo) {
        entityManager.remove(Video, video.id)
      }
    }

    // delete the subscription
    entityManager.remove(Subscription, subscriptions.id)
  }

  /**
   * Fetches the uploads playlist entity from the YouTube API for the specified
   * channel.
   *
   * @param channel The YouTube channel for which the uploads playlist should
   *        be fetched.
{'fraction_non_alphanumeric': 0.08898703251268558, 'fraction_numerical': 0.0007517383950385266, 'mean_word_length': 2.482657068062827, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '927027', 'n_tokens_mistral': 2731, 'n_tokens_neox': 2716, 'n_words': 727}