# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# Mobius Forensic Toolkit
# Copyright (C) 2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022 Eduardo Aguiar
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
import pymobius.operating_system
import pymobius.registry.installed_programs
import pymobius.p2p
import pymobius.p2p.account
import pymobius.p2p.application
import pymobius.p2p.search
import pymobius.p2p.remote_file
import pymobius.registry
import mobius
import binascii
from . import decoder_torrenth_dat
from . import decoder_tempdl_phash_dat
from . import profile
from pymobius.registry import *

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# References:
#    . Ares Galaxy 246 source code
#    . Forensic Analysis of Ares Galaxy Peer-to-Peer Network (Kolenbrander)
#
# Ares Galaxy main files (* decoded):
#
#  . DHTNodes.dat - DHT nodes
#       @see DHT_readnodeFile - DHT/dhtzones.pas (line 125)
#       (client ID, IP, udp_port, tcp_port, type)
#
#  . MDHTNodes.dat - MDHT nodes
#       @see MDHT_readnodeFile - BitTorrent/dht_zones.pas (line 124)
#       (client ID, IP, udp_port, type)
#
#  . PHashIdx.dat, PhashIdxTemp.dat, TempPHash.dat - PHash table
#       @see ICH_load_phash_indexs - helper_ICH.pas (line 1023)
#       (hash_sha1, Phash table)
#
#  * ShareH.dat - Trusted metadata
#       @see get_trusted_metas - helper_library_db.pas (line 542)
#
#  * ShareL.dat - Cached metadata
#       @see get_cached_metas - helper_library_db.pas (line 367)
#
#  . SNodes.dat
#       @see aresnodes_loadfromdisk - helper_ares_nodes (line 445)
#       (IP, port, reports, attempts, connects, first_seen, last_seen)
#
#  * TorrentH.dat - DHT magnet file history and metadata
#       @see tthread_dht.getMagnetFiles - DHT/thread_dht.pas (line 284)
#
#  . TempDL/PHash_XXX.dat - Downloading file pieces info
#       @see ICH_loadPieces - helper_ICH (line 528)
#       (flag_done, progress, hash_sha1)
#
#  . TempDL/PBTHash_XXX.dat - Downloading file (BitTorrent) metadata
#       @see BitTorrentDb_load - BitTorrent/BitTorrentDlDb.pas (line 88)
#
#  * ___ARESTRA___*.* - Downloading files, with metadata info
#       @see read_details_DB_Download - helper_download_disk.pas (line 722)
#
#  . __INCOMPLETE__*.* - Downloading files (BitTorrent)
#
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
import pymobius.p2p
import pymobius.operating_system
import mobius
from pymobius.p2p.local_file import *
import traceback

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Generic dataholder
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class dataholder (object):
  pass

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Iterate through Ares AppData folders
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def iter_ares_folders (item):
  for opsys in pymobius.operating_system.scan (item):
    for user_profile in opsys.get_profiles ():
      try:
        folder = user_profile.get_entry_by_path ('%localappdata%/Ares')
        if folder:
          yield user_profile, folder
      except Exception as e:
        mobius.core.logf ('WRN p2p.ares: ' + str (e))

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Retrieves Ares P2P activity data
# @author Eduardo Aguiar
# This function is planned to run in an independent thread. The idea here
# is to gather all activity data and only then write data to the model
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def retrieve (model):
  try:
    ant = Ant (model)
    ant.run ()
  except Exception as e:
    mobius.core.logf ('WRN %s %s' % (str (e), traceback.format_exc ()))

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief forensics: P2P Ares
# @author Eduardo Aguiar
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class Ant (object):

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Initialize object
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __init__ (self, model):
    self.__model = model	# P2P model
    self.__item = model.item

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Run
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def run (self):

    # create local data model
    self.__data = dataholder ()
    self.__data.application = pymobius.p2p.application.application ()
    self.__data.application.id = 'ares'
    self.__data.application.name = 'Ares Galaxy'
    self.__data.item = self.__model.item

    self.__data.accounts = []
    self.__data.searches = []
    self.__data.local_files = []
    self.__data.remote_files = []

    # retrieve data
    self.__retrieve ()

    # update P2P model
    self.__model.applications.append (self.__data.application)
    self.__model.accounts += self.__data.accounts
    self.__model.searches += self.__data.searches
    self.__model.local_files += self.__data.local_files
    self.__model.remote_files += self.__data.remote_files

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Retrieve data
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __retrieve (self):
    ant = pymobius.registry.main.Ant (self.__item)

    for registry in ant.get_data ():
      self.__retrieve_registry_data (registry)

    self.__retrieve_app_data ()
    self.__normalize_data ()

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Retrieve registry data
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __retrieve_registry_data (self, registry):

    # Application
    for program in pymobius.registry.installed_programs.get (registry):
      program_name = program.display_name.lower ()

      if program_name == 'ares' or program_name.startswith ('ares '):
        self.__data.application.versions.add (program.version)

    # Search History
    for username, user_key in pymobius.registry.iter_hkey_users (registry):
      for subkey in user_key.get_key_by_mask ('Software\\Ares\\Search.History\\*'):
        for value in subkey.values:
          keyword = binascii.unhexlify (value.name).decode ('utf-8')
          search = pymobius.p2p.search.search ()
          search.app_id = 'ares'
          search.app = 'Ares Galaxy'
          search.text = keyword
          search.username = username
          search.count = -1
          search.add_metadata ('Application', 'Ares Galaxy')
          search.add_metadata ('Search Term', search.text)
          search.add_metadata ('Category', subkey.name)
          self.__data.searches.append (search)

    # Accounts
    for username, user_key in pymobius.registry.iter_hkey_users (registry):
      ares_key = user_key.get_key_by_path ('Software\\Ares')

      if ares_key:
        guid = get_data_as_string (ares_key.get_data_by_name ('Personal.GUID'))
        account = pymobius.p2p.account.account ()
        account.username = username
        account.app_id = 'ares'
        account.app = 'Ares Galaxy'
        account.network = 'Ares'
        account.guid = guid
        self.__data.accounts.append (account)

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Retrieve data from disk files
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __retrieve_app_data (self):

    # scan Ares profiles
    for user_profile, folder in iter_ares_folders (self.__item):
      profile = pymobius.p2p.ares.profile.profile (self.__item)
      profile.username = user_profile.username

      # 'Data' folder
      data_folder = folder.get_child_by_name ('Data', False)
      profile.retrieve_data_folder (data_folder)

      # 'My Shared Folder'
      my_shared_folder = \
        user_profile.get_entry_by_path ('%desktop%/My Shared Folder') or \
        user_profile.get_entry_by_path ('%localappdata%/My Shared Folder')
      profile.retrieve_my_shared_folder (my_shared_folder)

      # Add to model
      self.__add_local_files (self.__item, self.__data, profile)
      self.__add_remote_files (self.__item, self.__data, profile)

      #self.__add_torrenth_dat (self.__item, self.__data, profile)

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Add data from profile.get_local_files
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __add_local_files (self, item, model, profile):

    # add metadata if it is not None
    def add_metadata (f, name, value):
      if value != None:
        f.add_metadata (name, value)

    # add local files
    for entry in profile.get_local_files ():
      lf = local_file ()
      lf.app_id = 'ares'
      lf.app = 'Ares Galaxy'
      lf.path = entry.path
      lf.size = entry.size if entry.size != None else -1
      lf.name = entry.name or entry.title
      lf.username = profile.username

      # flags
      if entry.flag_corrupted == STATE_YES:
        lf.flag_shared = STATE_NO 	# [Kolenbrander - Pg. 7]
      else:
        lf.flag_shared = entry.flag_shared

      lf.flag_downloaded = entry.flag_downloaded
      lf.flag_uploaded = entry.flag_uploaded
      lf.flag_completed = entry.flag_completed
      lf.flag_corrupted = entry.flag_corrupted

      # hashes
      if entry.hash_sha1:
        lf.set_hash ('sha1', entry.hash_sha1)

      # metadata
      metadata = entry.metadata

      lf.add_metadata ('Title', entry.title)
      lf.add_metadata ('Artist', entry.artist)
      lf.add_metadata ('Album', entry.album)
      lf.add_metadata ('Category', entry.category)
      lf.add_metadata ('Year', entry.year)
      lf.add_metadata ('Vidinfo', entry.vidinfo)
      lf.add_metadata ('Language', entry.language)
      lf.add_metadata ('URL', entry.url)
      lf.add_metadata ('Comment', entry.comment)
      lf.add_metadata ('Filetype', entry.filetype)

      add_metadata (lf, 'Download start time', metadata.get ('download_start_time'))
      add_metadata (lf, 'Download completed time', entry.download_completed_time)
      add_metadata (lf, 'Bytes downloaded', metadata.get ('bytes_downloaded'))
      add_metadata (lf, 'Bytes uploaded', metadata.get ('bytes_uploaded'))

      add_metadata (lf, 'Pieces', metadata.get ('pieces'))
      add_metadata (lf, 'Piece size', metadata.get ('piece_size'))
      add_metadata (lf, 'Pieces downloaded', metadata.get ('pieces_downloaded'))
      add_metadata (lf, 'Pieces to download', metadata.get ('pieces_to_download'))

      add_metadata (lf, 'ShareL.dat path', metadata.get ('sharel_path'))
      add_metadata (lf, 'ShareL.dat creation time (UTC)', metadata.get ('sharel_creation_time'))
      add_metadata (lf, 'ShareL.dat last modification time (UTC)', metadata.get ('sharel_last_modification_time'))

      add_metadata (lf, 'ShareH.dat path', metadata.get ('shareh_path'))
      add_metadata (lf, 'ShareH.dat creation time (UTC)', metadata.get ('shareh_creation_time'))
      add_metadata (lf, 'ShareH.dat last modification time (UTC)', metadata.get ('shareh_last_modification_time'))

      add_metadata (lf, 'Arestra subfolder', metadata.get ('arestra_subfolder'))
      add_metadata (lf, 'Arestra creation time (UTC)', metadata.get ('arestra_creation_time'))
      add_metadata (lf, 'Arestra last modification time (UTC)', metadata.get ('arestra_last_modification_time'))
      add_metadata (lf, 'Param1', metadata.get ('param1'))
      add_metadata (lf, 'Param2', metadata.get ('param2'))
      add_metadata (lf, 'Param3', metadata.get ('param3'))

      add_metadata (lf, 'PHash_.dat path', metadata.get ('phash_path'))
      add_metadata (lf, 'PHash_.dat creation time (UTC)', metadata.get ('phash_creation_time'))
      add_metadata (lf, 'PHash_.dat last modification time (UTC)', metadata.get ('phash_last_modification_time'))

      add_metadata (lf, 'PHashIdx.dat path', metadata.get ('phashidx_path'))
      add_metadata (lf, 'PHashIdx.dat creation time (UTC)', metadata.get ('phashidx_creation_time'))
      add_metadata (lf, 'PHashIdx.dat last modification time (UTC)', metadata.get ('phashidx_last_modification_time'))

      add_metadata (lf, 'Torrent hash SHA-1', metadata.get ('torrent_hash_sha1'))
      add_metadata (lf, 'Torrent piece length', metadata.get ('torrent_piece_length'))
      add_metadata (lf, 'Torrent pieces', metadata.get ('torrent_pieces'))
      add_metadata (lf, 'Torrent Files', metadata.get ('torrent_files'))
      add_metadata (lf, 'File index', metadata.get ('file_idx'))
      add_metadata (lf, 'Database time', metadata.get ('db_time'))
      add_metadata (lf, 'PBTHash_.dat path', metadata.get ('pbthash_path'))
      add_metadata (lf, 'PBTHash_.dat creation time (UTC)', metadata.get ('pbthash_creation_time'))
      add_metadata (lf, 'PBTHash_.dat last modification time (UTC)', metadata.get ('pbthash_last_modification_time'))

      add_metadata (lf, 'DHT magnet file added date/time', metadata.get ('dht_added_time'))
      add_metadata (lf, 'DHT magnet file SHA-1', metadata.get ('dht_hash_sha1'))
      add_metadata (lf, 'DHT magnet file verified', metadata.get ('dht_verified'))
      add_metadata (lf, 'Seeds', metadata.get ('seeds'))
      add_metadata (lf, 'TorrentH.dat path', metadata.get ('torrenth_path'))
      add_metadata (lf, 'TorrentH.dat creation time (UTC)', metadata.get ('torrenth_creation_time'))
      add_metadata (lf, 'TorrentH.dat last modification time (UTC)', metadata.get ('torrenth_last_modification_time'))

      # add to model
      model.local_files.append (lf)

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Add data from profile.get_remote_files
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __add_remote_files (self, item, model, profile):

    # add remote files
    for entry in profile.get_remote_files ():
      lf = pymobius.p2p.remote_file.remote_file ()
      lf.app_id = 'ares'
      lf.app = 'Ares Galaxy'
      lf.size = entry.size
      lf.name = entry.name
      lf.username = profile.username
      lf.timestamp = entry.timestamp

      # hash
      lf.set_hash ('sha1', entry.hash_sha1)

      # metadata
      if 'local_timestamp' in entry.metadata:
        lf.add_metadata ('Local timestamp', entry.metadata.get ('local_timestamp'))

      lf.add_metadata ('Title', entry.title)
      lf.add_metadata ('Artist', entry.artist)
      lf.add_metadata ('Album', entry.album)
      lf.add_metadata ('Category', entry.category)
      lf.add_metadata ('Year', entry.year)
      lf.add_metadata ('Language', entry.language)
      lf.add_metadata ('URL', entry.url)
      lf.add_metadata ('Comment', entry.comment)
      lf.add_metadata ('Filetype', entry.filetype)

      # peer data
      lf.peer.ip = entry.peer_ip
      lf.peer.port = entry.peer_port

      # add to model
      model.remote_files.append (lf)

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Add data from TorrentH.dat to model
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __add_torrenth_dat (self, item, model, profile):

    # add local files
    for entry in profile.torrenth.values ():
      lf = local_file ()
      lf.app_id = 'ares'
      lf.app = 'Ares Galaxy'
      lf.path = entry.path
      lf.size = entry.size
      lf.name = entry.name
      lf.username = profile.username

      # flags
      lf.flag_shared = STATE_YES		# @see DHT/thread_dht.pas (line 412) STATE_NO
      lf.flag_downloaded = STATE_YES		# @see DHT/dhtkeywords.pas (line 355)
      # @todo f.flag_uploaded
      lf.flag_completed = STATE_YES		# @see DHT/dhtkeywords.pas (line 355)
      lf.flag_corrupted = STATE_NO		# @see DHT/dhtkeywords.pas (line 355)

      # metadata
      lf.add_metadata ('Title', entry.title)
      lf.add_metadata ('Comment', entry.comment)
      lf.add_metadata ('Filetype', entry.filetype)
      lf.add_metadata ('DHT magnet file added date/time', entry.timestamp)
      lf.add_metadata ('DHT magnet file SHA-1', entry.hash_sha1)
      lf.add_metadata ('DHT magnet file verified', 'yes' if entry.flag_verified else 'no')
      lf.add_metadata ('URL', entry.url)
      lf.add_metadata ('Seeds', entry.seeds)
      lf.add_metadata ('TorrentH.dat path', entry.torrenth_path)
      lf.add_metadata ('TorrentH.dat creation time (UTC)', entry.torrenth_creation_time)
      lf.add_metadata ('TorrentH.dat last modification time (UTC)', entry.torrenth_last_modification_time)

      # add to model
      model.local_files.append (lf)

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Retrieve data from TempDL/PHash_XXX.dat
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __retrieve_tempdl_phash_dat (self, item, model, f, username):

    # decode file
    data = decoder_tempdl_phash_dat.decode (f)

    if not data:
      return

    # set file handled
    pymobius.p2p.set_handled (item, f)

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Add data from My Shared Folder/___ARESTRA___ files to model
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __add_arestra (self, item, model, profile):
    for entry in profile.arestra.values ():

      #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
      # create local file
      # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
      lf = local_file ()
      lf.app_id = 'ares'
      lf.app = 'Ares Galaxy'
      lf.path = entry.path
      lf.size = entry.size
      lf.name = entry.name
      lf.username = profile.username

      if entry.size > 0:
        pct_progress = float (entry.progress) * 100.0 / float (entry.size)
      else:
        pct_progress = 0.0

      # flags
      #lf.flag_shared = STATE_YES if entry.flag_shared else STATE_NO
      lf.flag_downloaded = STATE_YES
      # @todo f.flag_uploaded
      lf.flag_completed = STATE_YES if entry.progress == entry.size else STATE_NO
      lf.flag_corrupted = STATE_NO if entry.phash_verified == entry.progress else STATE_UNKNOWN

      # hashes
      lf.set_hash ('sha1', entry.hash_sha1)

      # metadata
      lf.add_metadata ('Signature', entry.signature)
      lf.add_metadata ('Version', entry.version)
      lf.add_metadata ('Bytes downloaded', '%d bytes (%.2f%%)' % (entry.progress, pct_progress))
      lf.add_metadata ('Genre', entry.kwgenre)
      lf.add_metadata ('Title', entry.title)
      lf.add_metadata ('Comments', entry.comment)
      lf.add_metadata ('Subfolder', entry.subfolder)
      lf.add_metadata ('Start time', entry.begin_time)
      lf.add_metadata ('File creation time (UTC)', entry.creation_time)
      lf.add_metadata ('File last modification time (UTC)', entry.last_modification_time)
      lf.add_metadata ('Flag paused', entry.flag_paused)
      lf.add_metadata ('Filetype', entry.filetype)
      lf.add_metadata ('Param1', entry.param1)
      lf.add_metadata ('Param2', entry.param2)
      lf.add_metadata ('Param3', entry.param3)

      # add to model
      model.local_files.append (lf)

      # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
      # create remote files
      # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
      for source in entry.sources:
        lf = pymobius.p2p.remote_file.remote_file ()
        lf.app_id = 'ares'
        lf.app = 'Ares Galaxy'
        lf.size = entry.size
        lf.name = entry.name
        lf.username = profile.username
        lf.timestamp = entry.creation_time

        # hash
        lf.set_hash ('sha1', entry.hash_sha1)

        # metadata
        lf.add_metadata ('Local timestamp', entry.begin_time)
        lf.add_metadata ('Genre', entry.kwgenre)
        lf.add_metadata ('Title', entry.title)
        lf.add_metadata ('Artist', entry.artist)
        lf.add_metadata ('Album', entry.album)
        lf.add_metadata ('Category', entry.category)
        lf.add_metadata ('Year', entry.year)
        lf.add_metadata ('Language', entry.language)
        lf.add_metadata ('URL', entry.url)
        lf.add_metadata ('Comments', entry.comment)
        lf.add_metadata ('Filetype', entry.filetype)

        # peer data
        lf.peer.ip = source.ip
        lf.peer.port = source.port

        # add to model
        model.remote_files.append (lf)

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Normalize retrieved data
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __normalize_data (self):

    # sort and remove duplicated remote files
    remote_files = [ (f.timestamp, f) for f in self.__data.remote_files ]
    timestamp, peer_ip, size, name = None, None, None, None
    self.__data.remote_files = []

    for (timestamp, f) in sorted (remote_files):
      if (timestamp, peer_ip, size, name) != (f.timestamp, f.peer.ip, f.size, f.name):
        self.__data.remote_files.append (f)
        timestamp, peer_ip, size, name = f.timestamp, f.peer.ip, f.size, f.name
