content: package de.danoeh.antennapod.net.download.service.feed.local;
  
  import android.content.Context;
  import android.media.MediaMetadataRetriever;
  import android.net.Uri;
  import android.text.TextUtils;
  import android.util.Log;
  
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  
  import org.apache.commons.io.FilenameUtils;
  import org.apache.commons.io.input.CountingInputStream;
  
  import java.io.BufferedInputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.text.ParseException;
  import java.text.SimpleDateFormat;
  import java.util.ArrayList;
  import java.util.Date;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Locale;
  import java.util.Set;
  import java.util.UUID;
  
  import androidx.annotation.VisibleForTesting;
  import androidx.documentfile.provider.DocumentFile;
  import de.danoeh.antennapod.model.MediaMetadataRetrieverCompat;
  import de.danoeh.antennapod.model.download.DownloadResult;
  import de.danoeh.antennapod.model.feed.FeedPreferences;
  import de.danoeh.antennapod.net.download.service.R;
  import de.danoeh.antennapod.storage.database.DBReader;
  import de.danoeh.antennapod.storage.database.FeedDatabaseWriter;
  import de.danoeh.antennapod.storage.database.DBWriter;
  import de.danoeh.antennapod.parser.feed.util.DateUtils;
  import de.danoeh.antennapod.model.download.DownloadError;
  import de.danoeh.antennapod.model.feed.Feed;
  import de.danoeh.antennapod.model.feed.FeedItem;
  import de.danoeh.antennapod.model.feed.FeedMedia;
  import de.danoeh.antennapod.model.playback.MediaType;
  import de.danoeh.antennapod.parser.feed.util.MimeTypeUtils;
  import de.danoeh.antennapod.parser.media.id3.ID3ReaderException;
  import de.danoeh.antennapod.parser.media.id3.Id3MetadataReader;
  import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentMetadataReader;
  import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentReaderException;
  
  public class LocalFeedUpdater {
      private static final String TAG = "LocalFeedUpdater";
  
      static final String[] PREFERRED_FEED_IMAGE_FILENAMES = {"folder.jpg", "Folder.jpg", "folder.png", "Folder.png"};
  
      public static Feed updateFeed(Feed feed, Context context,
                                    @Nullable UpdaterProgressListener updaterProgressListener) {
          try {
              String uriString = feed.getDownloadUrl().replace(Feed.PREFIX_LOCAL_FOLDER, "");
              DocumentFile documentFolder = DocumentFile.fromTreeUri(context, Uri.parse(uriString));
              if (documentFolder == null) {
                  throw new IOException("Unable to retrieve document tree. "
                          + "Try re-connecting the folder on the podcast info page.");
              }
              if (!documentFolder.exists() || !documentFolder.canRead()) {
                  throw new IOException("Cannot read local directory. "
                          + "Try re-connecting the folder on the podcast info page.");
              }
              Feed updatedFeed = tryUpdateFeed(feed, context, documentFolder.getUri(), updaterProgressListener);
  
              List<DownloadResult> downloadResults = DBReader.getFeedDownloadLog(feed.getId(), 1);
              if (downloadResults.isEmpty() || !downloadResults.get(0).isSuccessful()) {
                  reportSuccess(feed);
              }
              return updatedFeed;
          } catch (Exception e) {
              e.printStackTrace();
              reportError(feed, e.getMessage());
          }
          return null;
      }
  
      @VisibleForTesting
      static Feed tryUpdateFeed(Feed feed, Context context, Uri folderUri,
                                UpdaterProgressListener updaterProgressListener) throws IOException {
          if (feed.getItems() == null) {
              feed.setItems(new ArrayList<>());
          }
          // make sure it is the latest 'version' of this feed from the db (all items etc)
          // and for new feeds, settings etc are set up properly.
          feed = FeedDatabaseWriter.updateFeed(context, feed, false);
  
          // list files in feed folder
          List<FastDocumentFile> allFiles = FastDocumentFile.list(context, folderUri);
          List<FastDocumentFile> mediaFiles = new ArrayList<>();
          Set<String> mediaFileNames = new HashSet<>();
          for (FastDocumentFile file : allFiles) {
              String mimeType = MimeTypeUtils.getMimeType(file.getType(), file.getUri().toString());
              MediaType mediaType = MediaType.fromMimeType(mimeType);
              if (mediaType == MediaType.AUDIO || mediaType == MediaType.VIDEO) {
                  mediaFiles.add(file);
                  mediaFileNames.add(file.getName());
              }
          }
  
          // add new files to feed and update item data
          List<FeedItem> newItems = feed.getItems();
          for (int i = 0; i < mediaFiles.size(); i++) {
              FeedItem oldItem = feedContainsFile(feed, mediaFiles.get(i).getName());
              FeedItem newItem = createFeedItem(feed, mediaFiles.get(i), context);
              if (oldItem == null) {
                  newItems.add(newItem);
              } else {
                  oldItem.updateFromOther(newItem);
              }
              if (updaterProgressListener != null) {
                  updaterProgressListener.onLocalFileScanned(i, mediaFiles.size());
              }
          }
  
          // remove feed items without corresponding file
          Iterator<FeedItem> it = newItems.iterator();
          while (it.hasNext()) {
              FeedItem feedItem = it.next();
              if (!mediaFileNames.contains(feedItem.getLink())) {
                  it.remove();
              }
          }
  
          feed.setImageUrl(getImageUrl(allFiles, folderUri));
  
          feed.getPreferences().setAutoDownload(FeedPreferences.AutoDownloadSetting.DISABLED);
          feed.setDescription(context.getString(R.string.local_feed_description));
          feed.setAuthor(context.getString(R.string.local_folder));
  
          FeedDatabaseWriter.updateFeed(context, feed, true);
  
          return feed;
      }
  
      /**
       * Returns the image URL for the local feed.
       */
      @NonNull
      static String getImageUrl(List<FastDocumentFile> files, Uri folderUri) {
          // look for special file names
          for (String iconLocation : PREFERRED_FEED_IMAGE_FILENAMES) {
              for (FastDocumentFile file : files) {
                  if (iconLocation.equals(file.getName())) {
                      return file.getUri().toString();
                  }
              }
          }
  
          // use the first image in the folder if existing
          for (FastDocumentFile file : files) {
              String mime = file.getType();
              if (mime != null && (mime.startsWith("image/jpeg") || mime.startsWith("image/png"))) {
                  return file.getUri().toString();
              }
          }
  
          // use default icon as fallback
          return Feed.PREFIX_GENERATIVE_COVER + folderUri;
      }
  
      private static FeedItem feedContainsFile(Feed feed, String filename) {
          List<FeedItem> items = feed.getItems();
          for (FeedItem i : items) {
              if (i.getMedia() != null && i.getLink().equals(filename)) {
                  return i;
              }
          }
          return null;
      }
  
      private static FeedItem createFeedItem(Feed feed, FastDocumentFile file, Context context) {
          String title = FilenameUtils.removeExtension(file.getName());
          FeedItem item = new FeedItem(0, title, UUID.randomUUID().toString(),
                  file.getName(), new Date(file.getLastModified()), FeedItem.UNPLAYED, feed);
          item.disableAutoDownload();
  
          long size = file.getLength();
          FeedMedia media = new FeedMedia(0, item, 0, 0, size, file.getType(),
                  file.getUri().toString(), file.getUri().toString(), 0, null, 0, 0);
          item.setMedia(media);
  
          for (FeedItem existingItem : feed.getItems()) {
              if (existingItem.getMedia() != null
                      && existingItem.getMedia().getDownloadUrl().equals(file.getUri().toString())
                      && file.getLength() == existingItem.getMedia().getSize()) {
                  // We found an old file that we already scanned. Re-use metadata.
                  item.updateFromOther(existingItem);
                  return item;
              }
          }
  
          // Did not find existing item. Scan metadata.
          try {
              loadMetadata(item, file, context);
          } catch (Exception e) {
              e.printStackTrace();
              item.setDescriptionIfLonger(e.getMessage());
          }
          return item;
      }
  
      private static void loadMetadata(FeedItem item, FastDocumentFile file, Context context) {
          try (MediaMetadataRetrieverCompat mediaMetadataRetriever = new MediaMetadataRetrieverCompat()) {
              mediaMetadataRetriever.setDataSource(context, file.getUri());
  
              String dateStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE);
              if (!TextUtils.isEmpty(dateStr) && !"19040101T000000.000Z".equals(dateStr)) {
                  try {
                      SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.getDefault());
                      item.setPubDate(simpleDateFormat.parse(dateStr));
                  } catch (ParseException parseException) {
                      Date date = DateUtils.parse(dateStr);
                      if (date != null) {
                          item.setPubDate(date);
                      }
                  }
              }
  
              String title = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
              if (!TextUtils.isEmpty(title)) {
                  item.setTitle(title);
              }
  
              String durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
              if (durationStr != null && !durationStr.equals("null")) {
                  item.getMedia().setDuration((int) Long.parseLong(durationStr));
              }
  
              item.getMedia().setHasEmbeddedPicture(mediaMetadataRetriever.getEmbeddedPicture() != null);
  
              try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) {
                  Id3MetadataReader reader = new Id3MetadataReader(
                          new CountingInputStream(new BufferedInputStream(inputStream)));
                  reader.readInputStream();
                  item.setDescriptionIfLonger(reader.getComment());
              } catch (IOException | ID3ReaderException e) {
                  Log.d(TAG, "Unable to parse ID3 of " + file.getUri() + ": " + e.getMessage());
  
                  try (InputStream inputStream = context.getContentResolver().openInputStream(file.getUri())) {
                      VorbisCommentMetadataReader reader = new VorbisCommentMetadataReader(inputStream);
                      reader.readInputStream();
                      item.setDescriptionIfLonger(reader.getDescription());
                  } catch (IOException | VorbisCommentReaderException e2) {
                      Log.d(TAG, "Unable to parse vorbis comments of " + file.getUri() + ": " + e2.getMessage());
                  }
              }
          }
      }
  
      private static void reportError(Feed feed, String reasonDetailed) {
          DownloadResult status = new DownloadResult(feed.getTitle(), feed.getId(),
                  Feed.FEEDFILETYPE_FEED, false, DownloadError.ERROR_IO_ERROR, reasonDetailed);
          DBWriter.addDownloadStatus(status);
          DBWriter.setFeedLastUpdateFailed(feed.getId(), true);
      }
  
      /**
       * Reports a successful download status.
       */
      private static void reportSuccess(Feed feed) {
          DownloadResult status = new DownloadResult(feed.getTitle(), feed.getId(),
                  Feed.FEEDFILETYPE_FEED, true, DownloadError.SUCCESS, null);
          DBWriter.addDownloadStatus(status);
          DBWriter.setFeedLastUpdateFailed(feed.getId(), false);
      }
  
      @FunctionalInterface
      public interface UpdaterProgressListener {
          void onLocalFileScanned(int scanned, int totalFiles);
      }
  }
  