diff --git a/app/build.gradle b/app/build.gradle index 6b8d5dff7..85079604f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { resValue "string", "app_name", "NewPipe SponsorBlock" minSdk 19 targetSdk 29 - versionCode 986 - versionName "0.23.0" + versionCode 987 + versionName "0.23.1" multiDexEnabled true @@ -107,7 +107,7 @@ ext { icepickVersion = '3.2.0' exoPlayerVersion = '2.17.1' googleAutoServiceVersion = '1.0.1' - groupieVersion = '2.10.0' + groupieVersion = '2.10.1' markwonVersion = '4.6.2' leakCanaryVersion = '2.5' @@ -190,7 +190,7 @@ dependencies { // name and the commit hash with the commit hash of the (pushed) commit you want to test // This works thanks to JitPack: https://jitpack.io/ implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' - implementation 'com.github.TeamNewPipe:NewPipeExtractor:ac1c22d81c65b7b0c5427f4e1989f5256d617f32' + implementation 'com.github.TeamNewPipe:NewPipeExtractor:5219a705bab539cf8c6624d0cec216e76e85f0b1' /** Checkstyle **/ checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" @@ -221,10 +221,9 @@ dependencies { // https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01 implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.webkit:webkit:1.4.0' - implementation 'com.google.android.material:material:1.5.0' - implementation "androidx.work:work-runtime:${androidxWorkVersion}" implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}" implementation "androidx.work:work-rxjava3:${androidxWorkVersion}" + implementation 'com.google.android.material:material:1.5.0' /** Third-party libraries **/ // Instance state boilerplate elimination @@ -262,7 +261,7 @@ dependencies { implementation "com.nononsenseapps:filepicker:4.2.1" // Crash reporting - implementation "ch.acra:acra-core:5.8.4" + implementation "ch.acra:acra-core:5.9.3" // Properly restarting implementation 'com.jakewharton:process-phoenix:2.1.2' diff --git a/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt b/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt index a9aa40d82..016feb576 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/util/StreamItemAdapterTest.kt @@ -91,7 +91,12 @@ class StreamItemAdapterTest { context, StreamItemAdapter.StreamSizeWrapper( (0 until 5).map { - SubtitlesStream(MediaFormat.SRT, "pt-BR", "https://example.com", false) + SubtitlesStream.Builder() + .setContent("https://example.com", true) + .setMediaFormat(MediaFormat.SRT) + .setLanguageCode("pt-BR") + .setAutoGenerated(false) + .build() }, context ), @@ -108,7 +113,14 @@ class StreamItemAdapterTest { val adapter = StreamItemAdapter( context, StreamItemAdapter.StreamSizeWrapper( - (0 until 5).map { AudioStream("https://example.com/$it", MediaFormat.OPUS, 192) }, + (0 until 5).map { + AudioStream.Builder() + .setId(Stream.ID_UNKNOWN) + .setContent("https://example.com/$it", true) + .setMediaFormat(MediaFormat.OPUS) + .setAverageBitrate(192) + .build() + }, context ), null @@ -126,7 +138,13 @@ class StreamItemAdapterTest { private fun getVideoStreams(vararg videoOnly: Boolean) = StreamItemAdapter.StreamSizeWrapper( videoOnly.map { - VideoStream("https://example.com", MediaFormat.MPEG_4, "720p", it) + VideoStream.Builder() + .setId(Stream.ID_UNKNOWN) + .setContent("https://example.com", true) + .setMediaFormat(MediaFormat.MPEG_4) + .setResolution("720p") + .setIsVideoOnly(it) + .build() }, context ) @@ -138,8 +156,16 @@ class StreamItemAdapterTest { private fun getAudioStreams(vararg shouldBeValid: Boolean) = getSecondaryStreamsFromList( shouldBeValid.map { - if (it) AudioStream("https://example.com", MediaFormat.OPUS, 192) - else null + if (it) { + AudioStream.Builder() + .setId(Stream.ID_UNKNOWN) + .setContent("https://example.com", true) + .setMediaFormat(MediaFormat.OPUS) + .setAverageBitrate(192) + .build() + } else { + null + } } ) diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index 6b02e21ca..70c947478 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -205,7 +205,7 @@ public class App extends MultiDexApplication { return; } - final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder(this) + final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder() .withBuildConfigClass(BuildConfig.class); ACRA.init(this, acraConfig); } diff --git a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java index 5605d031d..f6bb14b31 100644 --- a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java +++ b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java @@ -43,7 +43,7 @@ import static org.schabi.newpipe.MainActivity.DEBUG; public final class DownloaderImpl extends Downloader { public static final String USER_AGENT - = "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0"; + = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"; public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY = "youtube_restricted_mode_key"; public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000"; diff --git a/app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt b/app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt index e9380c7b2..b9d1fe8f9 100644 --- a/app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt +++ b/app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt @@ -20,6 +20,7 @@ import org.schabi.newpipe.extractor.downloader.Response import org.schabi.newpipe.extractor.exceptions.ReCaptchaException import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired +import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk import org.schabi.newpipe.util.Version import java.io.IOException @@ -69,6 +70,11 @@ class NewVersionWorker( @Throws(IOException::class, ReCaptchaException::class) private fun checkNewVersion() { + // Check if the current apk is a github one or not. + if (!isReleaseApk()) { + return + } + val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext) // Check if the last request has happened a certain time ago // to reduce the number of API requests. diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index adef3c0e4..1fe6ce7ec 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -24,12 +24,12 @@ import android.widget.Toast; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.content.res.AppCompatResources; import androidx.core.app.NotificationCompat; import androidx.core.app.ServiceCompat; -import androidx.core.widget.TextViewCompat; import androidx.fragment.app.FragmentManager; import androidx.preference.PreferenceManager; @@ -58,7 +58,6 @@ import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException; import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.player.MainPlayer; @@ -71,7 +70,7 @@ import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.ExtractorHelper; -import org.schabi.newpipe.util.ListHelper; +import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.ThemeHelper; @@ -127,8 +126,10 @@ public class RouterActivity extends AppCompatActivity { } } + ThemeHelper.setDayNightMode(this); setTheme(ThemeHelper.isLightThemeSelected(this) ? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark); + Localization.assureCorrectAppLanguage(this); } @Override @@ -257,80 +258,122 @@ public class RouterActivity extends AppCompatActivity { protected void onSuccess() { final SharedPreferences preferences = PreferenceManager .getDefaultSharedPreferences(this); - final String selectedChoiceKey = preferences - .getString(getString(R.string.preferred_open_action_key), - getString(R.string.preferred_open_action_default)); - final String showInfoKey = getString(R.string.show_info_key); - final String videoPlayerKey = getString(R.string.video_player_key); - final String backgroundPlayerKey = getString(R.string.background_player_key); - final String popupPlayerKey = getString(R.string.popup_player_key); - final String downloadKey = getString(R.string.download_key); - final String alwaysAskKey = getString(R.string.always_ask_open_action_key); + final ChoiceAvailabilityChecker choiceChecker = new ChoiceAvailabilityChecker( + getChoicesForService(currentService, currentLinkType), + preferences.getString(getString(R.string.preferred_open_action_key), + getString(R.string.preferred_open_action_default))); - if (selectedChoiceKey.equals(alwaysAskKey)) { - final List choices - = getChoicesForService(currentService, currentLinkType); + // Check for non-player related choices + if (choiceChecker.isAvailableAndSelected( + R.string.show_info_key, + R.string.download_key, + R.string.add_to_playlist_key)) { + handleChoice(choiceChecker.getSelectedChoiceKey()); + return; + } + // Check if the choice is player related + if (choiceChecker.isAvailableAndSelected( + R.string.video_player_key, + R.string.background_player_key, + R.string.popup_player_key)) { + + final String selectedChoice = choiceChecker.getSelectedChoiceKey(); - switch (choices.size()) { - case 1: - handleChoice(choices.get(0).key); - break; - case 0: - handleChoice(showInfoKey); - break; - default: - showDialog(choices); - break; - } - } else if (selectedChoiceKey.equals(showInfoKey)) { - handleChoice(showInfoKey); - } else if (selectedChoiceKey.equals(downloadKey)) { - handleChoice(downloadKey); - } else { final boolean isExtVideoEnabled = preferences.getBoolean( getString(R.string.use_external_video_player_key), false); final boolean isExtAudioEnabled = preferences.getBoolean( getString(R.string.use_external_audio_player_key), false); - final boolean isVideoPlayerSelected = selectedChoiceKey.equals(videoPlayerKey) - || selectedChoiceKey.equals(popupPlayerKey); - final boolean isAudioPlayerSelected = selectedChoiceKey.equals(backgroundPlayerKey); + final boolean isVideoPlayerSelected = + selectedChoice.equals(getString(R.string.video_player_key)) + || selectedChoice.equals(getString(R.string.popup_player_key)); + final boolean isAudioPlayerSelected = + selectedChoice.equals(getString(R.string.background_player_key)); - if (currentLinkType != LinkType.STREAM) { - if (isExtAudioEnabled && isAudioPlayerSelected - || isExtVideoEnabled && isVideoPlayerSelected) { - Toast.makeText(this, R.string.external_player_unsupported_link_type, - Toast.LENGTH_LONG).show(); - handleChoice(showInfoKey); - return; - } + if (currentLinkType != LinkType.STREAM + && ((isExtAudioEnabled && isAudioPlayerSelected) + || (isExtVideoEnabled && isVideoPlayerSelected)) + ) { + Toast.makeText(this, R.string.external_player_unsupported_link_type, + Toast.LENGTH_LONG).show(); + handleChoice(getString(R.string.show_info_key)); + return; } - final List capabilities - = currentService.getServiceInfo().getMediaCapabilities(); + final List capabilities = + currentService.getServiceInfo().getMediaCapabilities(); - boolean serviceSupportsChoice = false; - if (isVideoPlayerSelected) { - serviceSupportsChoice = capabilities.contains(VIDEO); - } else if (selectedChoiceKey.equals(backgroundPlayerKey)) { - serviceSupportsChoice = capabilities.contains(AUDIO); - } - - if (serviceSupportsChoice) { - handleChoice(selectedChoiceKey); + // Check if the service supports the choice + if ((isVideoPlayerSelected && capabilities.contains(VIDEO)) + || (isAudioPlayerSelected && capabilities.contains(AUDIO))) { + handleChoice(selectedChoice); } else { - handleChoice(showInfoKey); + handleChoice(getString(R.string.show_info_key)); } + return; + } + + // Default / Ask always + final List availableChoices = choiceChecker.getAvailableChoices(); + switch (availableChoices.size()) { + case 1: + handleChoice(availableChoices.get(0).key); + break; + case 0: + handleChoice(getString(R.string.show_info_key)); + break; + default: + showDialog(availableChoices); + break; + } + } + + /** + * This is a helper class for checking if the choices are available and/or selected. + */ + class ChoiceAvailabilityChecker { + private final List availableChoices; + private final String selectedChoiceKey; + + ChoiceAvailabilityChecker( + @NonNull final List availableChoices, + @NonNull final String selectedChoiceKey) { + this.availableChoices = availableChoices; + this.selectedChoiceKey = selectedChoiceKey; + } + + public List getAvailableChoices() { + return availableChoices; + } + + public String getSelectedChoiceKey() { + return selectedChoiceKey; + } + + public boolean isAvailableAndSelected(@StringRes final int... wantedKeys) { + return Arrays.stream(wantedKeys).anyMatch(this::isAvailableAndSelected); + } + + public boolean isAvailableAndSelected(@StringRes final int wantedKey) { + final String wanted = getString(wantedKey); + // Check if the wanted option is selected + if (!selectedChoiceKey.equals(wanted)) { + return false; + } + // Check if it's available + return availableChoices.stream().anyMatch(item -> wanted.equals(item.key)); } } private void showDialog(final List choices) { final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - final Context themeWrapperContext = getThemeWrapperContext(); - final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext); - final RadioGroup radioGroup = SingleChoiceDialogViewBinding.inflate(getLayoutInflater()) - .list; + final Context themeWrapperContext = getThemeWrapperContext(); + final LayoutInflater layoutInflater = LayoutInflater.from(themeWrapperContext); + + final SingleChoiceDialogViewBinding binding = + SingleChoiceDialogViewBinding.inflate(layoutInflater); + final RadioGroup radioGroup = binding.list; final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> { final int indexOfChild = radioGroup.indexOfChild( @@ -349,21 +392,19 @@ public class RouterActivity extends AppCompatActivity { alertDialogChoice = new AlertDialog.Builder(themeWrapperContext) .setTitle(R.string.preferred_open_action_share_menu_title) - .setView(radioGroup) + .setView(binding.getRoot()) .setCancelable(true) .setNegativeButton(R.string.just_once, dialogButtonsClickListener) .setPositiveButton(R.string.always, dialogButtonsClickListener) - .setOnDismissListener((dialog) -> { + .setOnDismissListener(dialog -> { if (!selectionIsDownload && !selectionIsAddToPlaylist) { finish(); } }) .create(); - //noinspection CodeBlock2Expr - alertDialogChoice.setOnShowListener(dialog -> { - setDialogButtonsState(alertDialogChoice, radioGroup.getCheckedRadioButtonId() != -1); - }); + alertDialogChoice.setOnShowListener(dialog -> setDialogButtonsState( + alertDialogChoice, radioGroup.getCheckedRadioButtonId() != -1)); radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialogChoice, true)); @@ -383,9 +424,10 @@ public class RouterActivity extends AppCompatActivity { int id = 12345; for (final AdapterChoiceItem item : choices) { - final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot(); + final RadioButton radioButton = ListRadioIconItemBinding.inflate(layoutInflater) + .getRoot(); radioButton.setText(item.description); - TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(radioButton, + radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds( AppCompatResources.getDrawable(themeWrapperContext, item.icon), null, null, null); radioButton.setChecked(false); @@ -425,87 +467,64 @@ public class RouterActivity extends AppCompatActivity { private List getChoicesForService(final StreamingService service, final LinkType linkType) { - final Context context = getThemeWrapperContext(); - - final List returnList = new ArrayList<>(); - final List capabilities - = service.getServiceInfo().getMediaCapabilities(); - - final SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(this); - final boolean isExtVideoEnabled = preferences.getBoolean( - getString(R.string.use_external_video_player_key), false); - final boolean isExtAudioEnabled = preferences.getBoolean( - getString(R.string.use_external_audio_player_key), false); - - final AdapterChoiceItem videoPlayer = new AdapterChoiceItem( - getString(R.string.video_player_key), getString(R.string.video_player), - R.drawable.ic_play_arrow); final AdapterChoiceItem showInfo = new AdapterChoiceItem( getString(R.string.show_info_key), getString(R.string.show_info), R.drawable.ic_info_outline); - final AdapterChoiceItem popupPlayer = new AdapterChoiceItem( - getString(R.string.popup_player_key), getString(R.string.popup_player), - R.drawable.ic_picture_in_picture); + final AdapterChoiceItem videoPlayer = new AdapterChoiceItem( + getString(R.string.video_player_key), getString(R.string.video_player), + R.drawable.ic_play_arrow); final AdapterChoiceItem backgroundPlayer = new AdapterChoiceItem( getString(R.string.background_player_key), getString(R.string.background_player), R.drawable.ic_headset); - final AdapterChoiceItem addToPlaylist = new AdapterChoiceItem( - getString(R.string.add_to_playlist_key), getString(R.string.add_to_playlist), - R.drawable.ic_add); + final AdapterChoiceItem popupPlayer = new AdapterChoiceItem( + getString(R.string.popup_player_key), getString(R.string.popup_player), + R.drawable.ic_picture_in_picture); + final List returnedItems = new ArrayList<>(); + returnedItems.add(showInfo); // Always present + + final List capabilities = + service.getServiceInfo().getMediaCapabilities(); if (linkType == LinkType.STREAM) { - if (isExtVideoEnabled) { - // show both "show info" and "video player", they are two different activities - returnList.add(showInfo); - returnList.add(videoPlayer); - } else { - final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType(); - if (capabilities.contains(VIDEO) - && PlayerHelper.isAutoplayAllowedByUser(context) - && playerType == null || playerType == MainPlayer.PlayerType.VIDEO) { - // show only "video player" since the details activity will be opened and the - // video will be auto played there. Since "show info" would do the exact same - // thing, use that as a key to let VideoDetailFragment load the stream instead - // of using FetcherService (see comment in handleChoice()) - returnList.add(new AdapterChoiceItem( - showInfo.key, videoPlayer.description, videoPlayer.icon)); - } else { - // show only "show info" if video player is not applicable, auto play is - // disabled or a video is playing in a player different than the main one - returnList.add(showInfo); - } - } - if (capabilities.contains(VIDEO)) { - returnList.add(popupPlayer); + returnedItems.add(videoPlayer); + returnedItems.add(popupPlayer); } if (capabilities.contains(AUDIO)) { - returnList.add(backgroundPlayer); + returnedItems.add(backgroundPlayer); } // download is redundant for linkType CHANNEL AND PLAYLIST (till playlist downloading is // not supported ) - returnList.add(new AdapterChoiceItem(getString(R.string.download_key), + returnedItems.add(new AdapterChoiceItem(getString(R.string.download_key), getString(R.string.download), R.drawable.ic_file_download)); // Add to playlist is not necessary for CHANNEL and PLAYLIST linkType since those can // not be added to a playlist - returnList.add(addToPlaylist); - + returnedItems.add(new AdapterChoiceItem(getString(R.string.add_to_playlist_key), + getString(R.string.add_to_playlist), + R.drawable.ic_add)); } else { - returnList.add(showInfo); + // LinkType.NONE is never present because it's filtered out before + // channels and playlist can be played as they contain a list of videos + final SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(this); + final boolean isExtVideoEnabled = preferences.getBoolean( + getString(R.string.use_external_video_player_key), false); + final boolean isExtAudioEnabled = preferences.getBoolean( + getString(R.string.use_external_audio_player_key), false); + if (capabilities.contains(VIDEO) && !isExtVideoEnabled) { - returnList.add(videoPlayer); - returnList.add(popupPlayer); + returnedItems.add(videoPlayer); + returnedItems.add(popupPlayer); } if (capabilities.contains(AUDIO) && !isExtAudioEnabled) { - returnList.add(backgroundPlayer); + returnedItems.add(backgroundPlayer); } } - return returnList; + return returnedItems; } private Context getThemeWrapperContext() { @@ -567,7 +586,8 @@ public class RouterActivity extends AppCompatActivity { // stop and bypass FetcherService if InfoScreen was selected since // StreamDetailFragment can fetch data itself - if (selectedChoiceKey.equals(getString(R.string.show_info_key))) { + if (selectedChoiceKey.equals(getString(R.string.show_info_key)) + || canHandleChoiceLikeShowInfo(selectedChoiceKey)) { disposables.add(Observable .fromCallable(() -> NavigationHelper.getIntentByLink(this, currentUrl)) .subscribeOn(Schedulers.io()) @@ -590,6 +610,30 @@ public class RouterActivity extends AppCompatActivity { finish(); } + private boolean canHandleChoiceLikeShowInfo(final String selectedChoiceKey) { + if (!selectedChoiceKey.equals(getString(R.string.video_player_key))) { + return false; + } + // "video player" can be handled like "show info" (because VideoDetailFragment can load + // the stream instead of FetcherService) when... + + // ...Autoplay is enabled + if (!PlayerHelper.isAutoplayAllowedByUser(getThemeWrapperContext())) { + return false; + } + + final boolean isExtVideoEnabled = PreferenceManager.getDefaultSharedPreferences(this) + .getBoolean(getString(R.string.use_external_video_player_key), false); + // ...it's not done via an external player + if (isExtVideoEnabled) { + return false; + } + + // ...the player is not running or in normal Video-mode/type + final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType(); + return playerType == null || playerType == MainPlayer.PlayerType.VIDEO; + } + private void openAddToPlaylistDialog() { // Getting the stream info usually takes a moment // Notifying the user here to ensure that no confusion arises @@ -631,22 +675,13 @@ public class RouterActivity extends AppCompatActivity { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> { - final List sortedVideoStreams = ListHelper - .getSortedStreamVideosList(this, result.getVideoStreams(), - result.getVideoOnlyStreams(), false, false); - final int selectedVideoStreamIndex = ListHelper - .getDefaultResolutionIndex(this, sortedVideoStreams); + final DownloadDialog downloadDialog = new DownloadDialog(this, result); + downloadDialog.setOnDismissListener(dialog -> finish()); final FragmentManager fm = getSupportFragmentManager(); - final DownloadDialog downloadDialog = DownloadDialog.newInstance(result); - downloadDialog.setVideoStreams(sortedVideoStreams); - downloadDialog.setAudioStreams(result.getAudioStreams()); - downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); - downloadDialog.setOnDismissListener(dialog -> finish()); downloadDialog.show(fm, "downloadDialog"); fm.executePendingTransactions(); - }, throwable -> - showUnsupportedUrlDialog(currentUrl))); + }, throwable -> showUnsupportedUrlDialog(currentUrl))); } @Override @@ -672,8 +707,8 @@ public class RouterActivity extends AppCompatActivity { final int icon; AdapterChoiceItem(final String key, final String description, final int icon) { - this.description = description; this.key = key; + this.description = description; this.icon = icon; } } diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java index ad1941adb..a9d69afe8 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java @@ -4,7 +4,6 @@ import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.ForeignKey; -import androidx.room.Ignore; import androidx.room.Index; import org.schabi.newpipe.database.stream.model.StreamEntity; @@ -42,18 +41,19 @@ public class StreamHistoryEntity { @ColumnInfo(name = STREAM_REPEAT_COUNT) private long repeatCount; - public StreamHistoryEntity(final long streamUid, @NonNull final OffsetDateTime accessDate, + /** + * @param streamUid the stream id this history item will refer to + * @param accessDate the last time the stream was accessed + * @param repeatCount the total number of views this stream received + */ + public StreamHistoryEntity(final long streamUid, + @NonNull final OffsetDateTime accessDate, final long repeatCount) { this.streamUid = streamUid; this.accessDate = accessDate; this.repeatCount = repeatCount; } - @Ignore - public StreamHistoryEntity(final long streamUid, @NonNull final OffsetDateTime accessDate) { - this(streamUid, accessDate, 1); - } - public long getStreamUid() { return streamUid; } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt index a22fd2bb9..d8c19c1e9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt @@ -12,8 +12,7 @@ import org.schabi.newpipe.database.BasicDAO import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID import org.schabi.newpipe.extractor.stream.StreamType -import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM -import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM +import org.schabi.newpipe.util.StreamTypeUtil import java.time.OffsetDateTime @Dao @@ -91,8 +90,7 @@ abstract class StreamDAO : BasicDAO { ?: throw IllegalStateException("Stream cannot be null just after insertion.") newerStream.uid = existentMinimalStream.uid - val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM - if (!isNewerStreamLive) { + if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) { // Use the existent upload date if the newer stream does not have a better precision // (i.e. is an approximation). This is done to prevent unnecessary changes. diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 15ff83357..1f67d3788 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -69,9 +69,9 @@ import org.schabi.newpipe.util.VideoSegment; import java.io.File; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Objects; import icepick.Icepick; import icepick.State; @@ -83,6 +83,8 @@ import us.shandian.giga.service.DownloadManagerService; import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder; import us.shandian.giga.service.MissionState; +import static org.schabi.newpipe.extractor.stream.DeliveryMethod.PROGRESSIVE_HTTP; +import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; public class DownloadDialog extends DialogFragment @@ -93,17 +95,17 @@ public class DownloadDialog extends DialogFragment @State StreamInfo currentInfo; @State - StreamSizeWrapper wrappedAudioStreams = StreamSizeWrapper.empty(); + StreamSizeWrapper wrappedAudioStreams; @State - StreamSizeWrapper wrappedVideoStreams = StreamSizeWrapper.empty(); + StreamSizeWrapper wrappedVideoStreams; @State - StreamSizeWrapper wrappedSubtitleStreams = StreamSizeWrapper.empty(); + StreamSizeWrapper wrappedSubtitleStreams; @State - int selectedVideoIndex = 0; + int selectedVideoIndex; // set in the constructor @State - int selectedAudioIndex = 0; + int selectedAudioIndex = 0; // default to the first item @State - int selectedSubtitleIndex = 0; + int selectedSubtitleIndex = 0; // default to the first item @Nullable private OnDismissListener onDismissListener = null; @@ -146,81 +148,47 @@ public class DownloadDialog extends DialogFragment // Instance creation //////////////////////////////////////////////////////////////////////////*/ - public static DownloadDialog newInstance(final StreamInfo info) { - final DownloadDialog dialog = new DownloadDialog(); - dialog.setInfo(info); - return dialog; - } - - public static DownloadDialog newInstance(final Context context, final StreamInfo info) { - final ArrayList streamsList = new ArrayList<>(ListHelper - .getSortedStreamVideosList(context, info.getVideoStreams(), - info.getVideoOnlyStreams(), false, false)); - final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList); - - final DownloadDialog instance = newInstance(info); - instance.setVideoStreams(streamsList); - instance.setSelectedVideoStream(selectedStreamIndex); - instance.setAudioStreams(info.getAudioStreams()); - instance.setSubtitleStreams(info.getSubtitles()); - - return instance; - } - - - /*////////////////////////////////////////////////////////////////////////// - // Setters - //////////////////////////////////////////////////////////////////////////*/ - - private void setInfo(final StreamInfo info) { + /** + * Create a new download dialog with the video, audio and subtitle streams from the provided + * stream info. Video streams and video-only streams will be put into a single list menu, + * sorted according to their resolution and the default video resolution will be selected. + * + * @param context the context to use just to obtain preferences and strings (will not be stored) + * @param info the info from which to obtain downloadable streams and other info (e.g. title) + */ + public DownloadDialog(final Context context, @NonNull final StreamInfo info) { this.currentInfo = info; - } - public void setAudioStreams(final List audioStreams) { - setAudioStreams(new StreamSizeWrapper<>(audioStreams, getContext())); - } + // TODO: Adapt this code when the downloader support other types of stream deliveries + final List videoStreams = ListHelper.getSortedStreamVideosList( + context, + getStreamsOfSpecifiedDelivery(info.getVideoStreams(), PROGRESSIVE_HTTP), + getStreamsOfSpecifiedDelivery(info.getVideoOnlyStreams(), PROGRESSIVE_HTTP), + false, + false + ); - public void setAudioStreams(final StreamSizeWrapper was) { - this.wrappedAudioStreams = was; - } + this.wrappedVideoStreams = new StreamSizeWrapper<>(videoStreams, context); + this.wrappedAudioStreams = new StreamSizeWrapper<>( + getStreamsOfSpecifiedDelivery(info.getAudioStreams(), PROGRESSIVE_HTTP), context); + this.wrappedSubtitleStreams = new StreamSizeWrapper<>( + getStreamsOfSpecifiedDelivery(info.getSubtitles(), PROGRESSIVE_HTTP), context); - public void setVideoStreams(final List videoStreams) { - setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext())); - } - - public void setVideoStreams(final StreamSizeWrapper wvs) { - this.wrappedVideoStreams = wvs; - } - - public void setSubtitleStreams(final List subtitleStreams) { - setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext())); - } - - public void setSubtitleStreams( - final StreamSizeWrapper wss) { - this.wrappedSubtitleStreams = wss; - } - - public void setSelectedVideoStream(final int svi) { - this.selectedVideoIndex = svi; - } - - public void setSelectedAudioStream(final int sai) { - this.selectedAudioIndex = sai; - } - - public void setSelectedSubtitleStream(final int ssi) { - this.selectedSubtitleIndex = ssi; + this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams); } public void setVideoSegments(final VideoSegment[] seg) { this.segments = seg; } + /** + * @param onDismissListener the listener to call in {@link #onDismiss(DialogInterface)} + */ public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) { this.onDismissListener = onDismissListener; } + /*////////////////////////////////////////////////////////////////////////// // Android lifecycle //////////////////////////////////////////////////////////////////////////*/ @@ -256,11 +224,16 @@ public class DownloadDialog extends DialogFragment .getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i)); if (audioStream != null) { - secondaryStreams - .append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream)); + secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, + audioStream)); } else if (DEBUG) { - Log.w(TAG, "No audio stream candidates for video format " - + videoStreams.get(i).getFormat().name()); + final MediaFormat mediaFormat = videoStreams.get(i).getFormat(); + if (mediaFormat != null) { + Log.w(TAG, "No audio stream candidates for video format " + + mediaFormat.name()); + } else { + Log.w(TAG, "No audio stream candidates for unknown video format"); + } } } @@ -295,7 +268,8 @@ public class DownloadDialog extends DialogFragment } @Override - public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, + public View onCreateView(@NonNull final LayoutInflater inflater, + final ViewGroup container, final Bundle savedInstanceState) { if (DEBUG) { Log.d(TAG, "onCreateView() called with: " @@ -306,14 +280,15 @@ public class DownloadDialog extends DialogFragment } @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + public void onViewCreated(@NonNull final View view, + @Nullable final Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); dialogBinding = DownloadDialogBinding.bind(view); dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName())); selectedAudioIndex = ListHelper - .getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams()); + .getDefaultAudioFormat(getContext(), wrappedAudioStreams.getStreamsList()); selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll()); @@ -331,7 +306,8 @@ public class DownloadDialog extends DialogFragment dialogBinding.threads.setProgress(threads - 1); dialogBinding.threads.setOnSeekBarChangeListener(new SimpleOnSeekBarChangeListener() { @Override - public void onProgressChanged(@NonNull final SeekBar seekbar, final int progress, + public void onProgressChanged(@NonNull final SeekBar seekbar, + final int progress, final boolean fromUser) { final int newProgress = progress + 1; prefs.edit().putInt(getString(R.string.default_download_threads), newProgress) @@ -476,7 +452,7 @@ public class DownloadDialog extends DialogFragment result, getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO); } - private void requestDownloadSaveAsResult(final ActivityResult result) { + private void requestDownloadSaveAsResult(@NonNull final ActivityResult result) { if (result.getResultCode() != Activity.RESULT_OK) { return; } @@ -493,8 +469,8 @@ public class DownloadDialog extends DialogFragment return; } - final DocumentFile docFile - = DocumentFile.fromSingleUri(context, result.getData().getData()); + final DocumentFile docFile = DocumentFile.fromSingleUri(context, + result.getData().getData()); if (docFile == null) { showFailedDialog(R.string.general_error); return; @@ -505,7 +481,7 @@ public class DownloadDialog extends DialogFragment docFile.getType()); } - private void requestDownloadPickFolderResult(final ActivityResult result, + private void requestDownloadPickFolderResult(@NonNull final ActivityResult result, final String key, final String tag) { if (result.getResultCode() != Activity.RESULT_OK) { @@ -525,12 +501,11 @@ public class DownloadDialog extends DialogFragment StoredDirectoryHelper.PERMISSION_FLAGS); } - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putString(key, uri.toString()).apply(); + PreferenceManager.getDefaultSharedPreferences(context).edit().putString(key, + uri.toString()).apply(); try { - final StoredDirectoryHelper mainStorage - = new StoredDirectoryHelper(context, uri, tag); + final StoredDirectoryHelper mainStorage = new StoredDirectoryHelper(context, uri, tag); checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp, mimeTmp); } catch (final IOException e) { @@ -568,8 +543,10 @@ public class DownloadDialog extends DialogFragment } @Override - public void onItemSelected(final AdapterView parent, final View view, - final int position, final long id) { + public void onItemSelected(final AdapterView parent, + final View view, + final int position, + final long id) { if (DEBUG) { Log.d(TAG, "onItemSelected() called with: " + "parent = [" + parent + "], view = [" + view + "], " @@ -604,14 +581,16 @@ public class DownloadDialog extends DialogFragment final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0; final boolean isSubtitleStreamsAvailable = subtitleStreamsAdapter.getCount() > 0; - dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE); - dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE); + dialogBinding.audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE + : View.GONE); + dialogBinding.videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE + : View.GONE); dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable ? View.VISIBLE : View.GONE); prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type), - getString(R.string.last_download_type_video_key)); + getString(R.string.last_download_type_video_key)); if (isVideoStreamsAvailable && (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) { @@ -647,7 +626,7 @@ public class DownloadDialog extends DialogFragment dialogBinding.subtitleButton.setEnabled(enabled); } - private int getSubtitleIndexBy(final List streams) { + private int getSubtitleIndexBy(@NonNull final List streams) { final Localization preferredLocalization = NewPipe.getPreferredLocalization(); int candidate = 0; @@ -673,8 +652,10 @@ public class DownloadDialog extends DialogFragment return candidate; } + @NonNull private String getNameEditText() { - final String str = dialogBinding.fileName.getText().toString().trim(); + final String str = Objects.requireNonNull(dialogBinding.fileName.getText()).toString() + .trim(); return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str); } @@ -690,12 +671,8 @@ public class DownloadDialog extends DialogFragment } private void launchDirectoryPicker(final ActivityResultLauncher launcher) { - NoFileManagerSafeGuard.launchSafe( - launcher, - StoredDirectoryHelper.getPicker(context), - TAG, - context - ); + NoFileManagerSafeGuard.launchSafe(launcher, StoredDirectoryHelper.getPicker(context), TAG, + context); } private void prepareSelectedDownload() { @@ -716,7 +693,7 @@ public class DownloadDialog extends DialogFragment if (format == MediaFormat.WEBMA_OPUS) { mimeTmp = "audio/ogg"; filenameTmp += "opus"; - } else { + } else if (format != null) { mimeTmp = format.mimeType; filenameTmp += format.suffix; } @@ -725,22 +702,30 @@ public class DownloadDialog extends DialogFragment selectedMediaType = getString(R.string.last_download_type_video_key); mainStorage = mainStorageVideo; format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat(); - mimeTmp = format.mimeType; - filenameTmp += format.suffix; + if (format != null) { + mimeTmp = format.mimeType; + filenameTmp += format.suffix; + } break; case R.id.subtitle_button: selectedMediaType = getString(R.string.last_download_type_subtitle_key); mainStorage = mainStorageVideo; // subtitle & video files go together format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat(); - mimeTmp = format.mimeType; - filenameTmp += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix; + if (format != null) { + mimeTmp = format.mimeType; + } + + if (format == MediaFormat.TTML) { + filenameTmp += MediaFormat.SRT.suffix; + } else if (format != null) { + filenameTmp += format.suffix; + } break; default: throw new RuntimeException("No stream selected"); } - if (!askForSavePath - && (mainStorage == null + if (!askForSavePath && (mainStorage == null || mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context) || mainStorage.isInvalidSafStorage())) { // Pick new download folder if one of: @@ -774,18 +759,16 @@ public class DownloadDialog extends DialogFragment initialPath = Uri.parse(initialSavePath.getAbsolutePath()); } - NoFileManagerSafeGuard.launchSafe( - requestDownloadSaveAsLauncher, - StoredFileHelper.getNewPicker(context, filenameTmp, mimeTmp, initialPath), - TAG, - context - ); + NoFileManagerSafeGuard.launchSafe(requestDownloadSaveAsLauncher, + StoredFileHelper.getNewPicker(context, filenameTmp, mimeTmp, initialPath), TAG, + context); return; } // check for existing file with the same name - checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp, mimeTmp); + checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp, + mimeTmp); // remember the last media type downloaded by the user prefs.edit().putString(getString(R.string.last_used_download_type), selectedMediaType) @@ -793,7 +776,8 @@ public class DownloadDialog extends DialogFragment } private void checkSelectedDownload(final StoredDirectoryHelper mainStorage, - final Uri targetFile, final String filename, + final Uri targetFile, + final String filename, final String mime) { StoredFileHelper storage; @@ -954,7 +938,7 @@ public class DownloadDialog extends DialogFragment storage.truncate(); } } catch (final IOException e) { - Log.e(TAG, "failed to truncate the file: " + storage.getUri().toString(), e); + Log.e(TAG, "Failed to truncate the file: " + storage.getUri().toString(), e); showFailedDialog(R.string.overwrite_failed); return; } @@ -999,8 +983,8 @@ public class DownloadDialog extends DialogFragment } psArgs = null; - final long videoSize = wrappedVideoStreams - .getSizeInBytes((VideoStream) selectedStream); + final long videoSize = wrappedVideoStreams.getSizeInBytes( + (VideoStream) selectedStream); // set nearLength, only, if both sizes are fetched or known. This probably // does not work on slow networks but is later updated in the downloader @@ -1016,7 +1000,7 @@ public class DownloadDialog extends DialogFragment if (selectedStream.getFormat() == MediaFormat.TTML) { psName = Postprocessing.ALGORITHM_TTML_CONVERTER; - psArgs = new String[]{ + psArgs = new String[] { selectedStream.getFormat().getSuffix(), "false" // ignore empty frames }; @@ -1027,17 +1011,22 @@ public class DownloadDialog extends DialogFragment } if (secondaryStream == null) { - urls = new String[]{ - selectedStream.getUrl() + urls = new String[] { + selectedStream.getContent() }; - recoveryInfo = new MissionRecoveryInfo[]{ + recoveryInfo = new MissionRecoveryInfo[] { new MissionRecoveryInfo(selectedStream) }; } else { - urls = new String[]{ - selectedStream.getUrl(), secondaryStream.getUrl() + if (secondaryStream.getDeliveryMethod() != PROGRESSIVE_HTTP) { + throw new IllegalArgumentException("Unsupported stream delivery format" + + secondaryStream.getDeliveryMethod()); + } + + urls = new String[] { + selectedStream.getContent(), secondaryStream.getContent() }; - recoveryInfo = new MissionRecoveryInfo[]{new MissionRecoveryInfo(selectedStream), + recoveryInfo = new MissionRecoveryInfo[] {new MissionRecoveryInfo(selectedStream), new MissionRecoveryInfo(secondaryStream)}; } diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt index b2ba912ec..f9f9f003a 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt @@ -7,13 +7,13 @@ import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.schabi.newpipe.R import org.schabi.newpipe.extractor.Info -import org.schabi.newpipe.extractor.NewPipe import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException import org.schabi.newpipe.extractor.exceptions.ExtractionException import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException import org.schabi.newpipe.ktx.isNetworkRelated +import org.schabi.newpipe.util.ServiceHelper import java.io.PrintWriter import java.io.StringWriter @@ -65,7 +65,7 @@ class ErrorInfo( constructor(throwable: Throwable, userAction: UserAction, request: String) : this(throwable, userAction, SERVICE_NONE, request) constructor(throwable: Throwable, userAction: UserAction, request: String, serviceId: Int) : - this(throwable, userAction, NewPipe.getNameOfService(serviceId), request) + this(throwable, userAction, ServiceHelper.getNameOfServiceById(serviceId), request) constructor(throwable: Throwable, userAction: UserAction, request: String, info: Info?) : this(throwable, userAction, getInfoServiceName(info), request) @@ -73,7 +73,7 @@ class ErrorInfo( constructor(throwable: List, userAction: UserAction, request: String) : this(throwable, userAction, SERVICE_NONE, request) constructor(throwable: List, userAction: UserAction, request: String, serviceId: Int) : - this(throwable, userAction, NewPipe.getNameOfService(serviceId), request) + this(throwable, userAction, ServiceHelper.getNameOfServiceById(serviceId), request) constructor(throwable: List, userAction: UserAction, request: String, info: Info?) : this(throwable, userAction, getInfoServiceName(info), request) @@ -95,7 +95,7 @@ class ErrorInfo( Array(throwable.size) { i -> getStackTrace(throwable[i]) } private fun getInfoServiceName(info: Info?) = - if (info == null) SERVICE_NONE else NewPipe.getNameOfService(info.serviceId) + if (info == null) SERVICE_NONE else ServiceHelper.getNameOfServiceById(info.serviceId) @StringRes private fun getMessageStringId( diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt index 692cb427a..15343f53d 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt @@ -15,7 +15,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.Disposable import org.schabi.newpipe.MainActivity import org.schabi.newpipe.R -import org.schabi.newpipe.extractor.NewPipe import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException @@ -106,7 +105,7 @@ class ErrorPanelHelper( if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) { errorServiceInfoTextView.text = context.resources.getString( R.string.service_provides_reason, - NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context)) + ServiceHelper.getSelectedService(context)?.serviceInfo?.name ?: "" ) errorServiceInfoTextView.isVisible = true diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 80eb7fe3b..de3f5f30c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -31,6 +31,7 @@ import android.view.WindowManager; import android.view.animation.DecelerateInterpolator; import android.widget.FrameLayout; import android.widget.RelativeLayout; +import android.widget.Toast; import androidx.annotation.AttrRes; import androidx.annotation.NonNull; @@ -97,6 +98,7 @@ import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; import org.schabi.newpipe.util.PicassoHelper; +import org.schabi.newpipe.util.StreamTypeUtil; import org.schabi.newpipe.util.SponsorBlockUtils; import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.ThemeHelper; @@ -125,6 +127,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientat import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired; import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET; import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView; +import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams; public final class VideoDetailFragment extends BaseStateFragment @@ -192,8 +195,6 @@ public final class VideoDetailFragment @Nullable private Disposable videoSegmentsSubscriber = null; - private List sortedVideoStreams; - private int selectedVideoStreamIndex = -1; private BottomSheetBehavior bottomSheetBehavior; private BroadcastReceiver broadcastReceiver; @@ -672,8 +673,7 @@ public final class VideoDetailFragment binding.detailControlsCrashThePlayer.setOnClickListener( v -> VideoDetailPlayerCrasher.onCrashThePlayer( this.getContext(), - this.player, - getLayoutInflater()) + this.player) ); } @@ -1102,9 +1102,6 @@ public final class VideoDetailFragment } private void openBackgroundPlayer(final boolean append) { - final AudioStream audioStream = currentInfo.getAudioStreams() - .get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams())); - final boolean useExternalAudioPlayer = PreferenceManager .getDefaultSharedPreferences(activity) .getBoolean(activity.getString(R.string.use_external_audio_player_key), false); @@ -1119,7 +1116,17 @@ public final class VideoDetailFragment if (!useExternalAudioPlayer) { openNormalBackgroundPlayer(append); } else { - startOnExternalPlayer(activity, currentInfo, audioStream); + final List audioStreams = getUrlAndNonTorrentStreams( + currentInfo.getAudioStreams()); + final int index = ListHelper.getDefaultAudioFormat(activity, audioStreams); + + if (index == -1) { + Toast.makeText(activity, R.string.no_audio_streams_available_for_external_players, + Toast.LENGTH_SHORT).show(); + return; + } + + startOnExternalPlayer(activity, currentInfo, audioStreams.get(index)); } } @@ -1635,14 +1642,6 @@ public final class VideoDetailFragment binding.detailToggleSecondaryControlsView.setVisibility(View.VISIBLE); binding.detailSecondaryControlPanel.setVisibility(View.GONE); - sortedVideoStreams = ListHelper.getSortedStreamVideosList( - activity, - info.getVideoStreams(), - info.getVideoOnlyStreams(), - false, - false); - selectedVideoStreamIndex = ListHelper - .getDefaultResolutionIndex(activity, sortedVideoStreams); updateProgressInfo(info); initThumbnailViews(info); showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView, @@ -1668,8 +1667,8 @@ public final class VideoDetailFragment } } - binding.detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM - || info.getStreamType() == StreamType.AUDIO_LIVE_STREAM ? View.GONE : View.VISIBLE); + binding.detailControlsDownload.setVisibility( + StreamTypeUtil.isLiveStream(info.getStreamType()) ? View.GONE : View.VISIBLE); binding.detailControlsBackground.setVisibility(info.getAudioStreams().isEmpty() ? View.GONE : View.VISIBLE); @@ -1727,18 +1726,12 @@ public final class VideoDetailFragment .observeOn(AndroidSchedulers.mainThread()) .subscribe(videoSegments -> { try { - final DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo); - downloadDialog.setVideoStreams(sortedVideoStreams); - downloadDialog.setAudioStreams(currentInfo.getAudioStreams()); - downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); - downloadDialog.setSubtitleStreams(currentInfo.getSubtitles()); + final DownloadDialog downloadDialog = new DownloadDialog(activity, currentInfo); downloadDialog.setVideoSegments(videoSegments); downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog"); } catch (final Exception e) { - ErrorUtil.showSnackbar(activity, - new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG, - "Showing download dialog", - currentInfo)); + ErrorUtil.showSnackbar(activity, new ErrorInfo(e, UserAction.DOWNLOAD_OPEN_DIALOG, + "Showing download dialog", currentInfo)); } }); } @@ -1765,8 +1758,7 @@ public final class VideoDetailFragment binding.detailPositionView.setVisibility(View.GONE); // TODO: Remove this check when separation of concerns is done. // (live streams weren't getting updated because they are mixed) - if (!info.getStreamType().equals(StreamType.LIVE_STREAM) - && !info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) { + if (!StreamTypeUtil.isLiveStream(info.getStreamType())) { return; } } else { @@ -2194,25 +2186,52 @@ public final class VideoDetailFragment } private void showExternalPlaybackDialog() { - if (sortedVideoStreams == null) { + if (currentInfo == null) { return; } - final CharSequence[] resolutions = new CharSequence[sortedVideoStreams.size()]; - for (int i = 0; i < sortedVideoStreams.size(); i++) { - resolutions[i] = sortedVideoStreams.get(i).getResolution(); - } - final AlertDialog.Builder builder = new AlertDialog.Builder(activity) - .setNegativeButton(R.string.cancel, null) - .setNeutralButton(R.string.open_in_browser, (dialog, i) -> - ShareUtils.openUrlInBrowser(requireActivity(), url) + + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle(R.string.select_quality_external_players); + builder.setNeutralButton(R.string.open_in_browser, (dialog, i) -> + ShareUtils.openUrlInBrowser(requireActivity(), url)); + + final List videoStreamsForExternalPlayers = + ListHelper.getSortedStreamVideosList( + activity, + getUrlAndNonTorrentStreams(currentInfo.getVideoStreams()), + getUrlAndNonTorrentStreams(currentInfo.getVideoOnlyStreams()), + false, + false ); - // Maybe there are no video streams available, show just `open in browser` button - if (resolutions.length > 0) { - builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndex, (dialog, i) -> { - dialog.dismiss(); - startOnExternalPlayer(activity, currentInfo, sortedVideoStreams.get(i)); - } - ); + + if (videoStreamsForExternalPlayers.isEmpty()) { + builder.setMessage(R.string.no_video_streams_available_for_external_players); + builder.setPositiveButton(R.string.ok, null); + + } else { + final int selectedVideoStreamIndexForExternalPlayers = + ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers); + final CharSequence[] resolutions = + new CharSequence[videoStreamsForExternalPlayers.size()]; + + for (int i = 0; i < videoStreamsForExternalPlayers.size(); i++) { + resolutions[i] = videoStreamsForExternalPlayers.get(i).getResolution(); + } + + builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers, + null); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.ok, (dialog, i) -> { + final int index = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); + // We don't have to manage the index validity because if there is no stream + // available for external players, this code will be not executed and if there is + // no stream which matches the default resolution, 0 is returned by + // ListHelper.getDefaultResolutionIndex. + // The index cannot be outside the bounds of the list as its always between 0 and + // the list size - 1, . + startOnExternalPlayer(activity, currentInfo, + videoStreamsForExternalPlayers.get(index)); + }); } builder.show(); } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java index ae704e88c..55336a42f 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java @@ -1,5 +1,9 @@ package org.schabi.newpipe.fragments.detail; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_DECODING_FAILED; +import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECIFIED; + import android.content.Context; import android.util.Log; import android.view.ContextThemeWrapper; @@ -29,10 +33,6 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Supplier; -import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW; -import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_DECODING_FAILED; -import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECIFIED; - /** * Outsourced logic for crashing the player in the {@link VideoDetailFragment}. */ @@ -97,8 +97,7 @@ public final class VideoDetailPlayerCrasher { public static void onCrashThePlayer( @NonNull final Context context, - @Nullable final Player player, - @NonNull final LayoutInflater layoutInflater + @Nullable final Player player ) { if (player == null) { Log.d(TAG, "Player is not available"); @@ -109,16 +108,15 @@ public final class VideoDetailPlayerCrasher { } // -- Build the dialog/UI -- - final Context themeWrapperContext = getThemeWrapperContext(context); - final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext); - final RadioGroup radioGroup = SingleChoiceDialogViewBinding.inflate(layoutInflater) - .list; - final AlertDialog alertDialog = new AlertDialog.Builder(getThemeWrapperContext(context)) + final SingleChoiceDialogViewBinding binding = + SingleChoiceDialogViewBinding.inflate(inflater); + + final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapperContext) .setTitle("Choose an exception") - .setView(radioGroup) + .setView(binding.getRoot()) .setCancelable(true) .setNegativeButton(R.string.cancel, null) .create(); @@ -136,11 +134,9 @@ public final class VideoDetailPlayerCrasher { ); radioButton.setOnClickListener(v -> { tryCrashPlayerWith(player, entry.getValue().get()); - if (alertDialog != null) { - alertDialog.cancel(); - } + alertDialog.cancel(); }); - radioGroup.addView(radioButton); + binding.list.addView(radioButton); } alertDialog.show(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 869503b5b..fa8f5fdbd 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -77,6 +77,8 @@ public class ChannelFragment extends BaseListInfoFragment dialog.show(getFM(), TAG) + )); + break; default: return super.onOptionsItemSelected(item); } @@ -293,10 +305,8 @@ public class PlaylistFragment extends BaseListInfoFragment 0 - && item.getStreamType() != StreamType.LIVE_STREAM) { + && !StreamTypeUtil.isLiveStream(item.getStreamType())) { itemProgressView.setMax((int) item.getDuration()); if (itemProgressView.getVisibility() == View.VISIBLE) { itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS diff --git a/app/src/main/java/org/schabi/newpipe/ktx/View.kt b/app/src/main/java/org/schabi/newpipe/ktx/View.kt index 8dcc9d85c..ace1dbf7e 100644 --- a/app/src/main/java/org/schabi/newpipe/ktx/View.kt +++ b/app/src/main/java/org/schabi/newpipe/ktx/View.kt @@ -300,14 +300,7 @@ private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long, } } -fun View.slideUp( - duration: Long, - delay: Long, - @FloatRange(from = 0.0, to = 1.0) translationPercent: Float -) { - slideUp(duration, delay, translationPercent, null) -} - +@JvmOverloads fun View.slideUp( duration: Long, delay: Long = 0L, diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index e97629f31..b291aa035 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -25,7 +25,6 @@ import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.graphics.Typeface -import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable import android.os.Bundle import android.os.Parcelable @@ -37,7 +36,6 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.Button -import androidx.annotation.AttrRes import androidx.annotation.Nullable import androidx.appcompat.app.AlertDialog import androidx.appcompat.content.res.AppCompatResources @@ -80,6 +78,7 @@ import org.schabi.newpipe.util.DeviceUtils import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams +import org.schabi.newpipe.util.ThemeHelper.resolveDrawable import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout import java.time.OffsetDateTime import java.util.function.Consumer @@ -579,19 +578,6 @@ class FeedFragment : BaseStateFragment() { lastNewItemsCount = highlightCount } - private fun resolveDrawable(context: Context, @AttrRes attrResId: Int): Drawable? { - return androidx.core.content.ContextCompat.getDrawable( - context, - android.util.TypedValue().apply { - context.theme.resolveAttribute( - attrResId, - this, - true - ) - }.resourceId - ) - } - private fun showNewItemsLoaded() { tryGetNewItemsLoadedButton()?.clearAnimation() tryGetNewItemsLoadedButton() diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt b/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt index 217e3f3e3..96d395aa5 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt @@ -14,6 +14,8 @@ import org.schabi.newpipe.databinding.ListStreamItemBinding import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM +import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_AUDIO_STREAM +import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.PicassoHelper @@ -109,7 +111,7 @@ data class StreamItem( } override fun isLongClickable() = when (stream.streamType) { - AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM -> true + AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM, POST_LIVE_STREAM, POST_LIVE_AUDIO_STREAM -> true else -> false } diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index 45445cf58..19f7afce5 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java @@ -128,13 +128,11 @@ public class HistoryRecordManager { // Add a history entry final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId); - if (latestEntry != null) { - streamHistoryTable.delete(latestEntry); - latestEntry.setAccessDate(currentTime); - latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1); - return streamHistoryTable.insert(latestEntry); + if (latestEntry == null) { + // never actually viewed: add history entry but with 0 views + return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime, 0)); } else { - return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime)); + return 0L; } })).subscribeOn(Schedulers.io()); } @@ -155,7 +153,8 @@ public class HistoryRecordManager { latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1); return streamHistoryTable.insert(latestEntry); } else { - return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime)); + // just viewed for the first time: set 1 view + return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime, 1)); } })).subscribeOn(Schedulers.io()); } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java index 561cde560..d39758326 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java @@ -11,12 +11,12 @@ import androidx.core.content.ContextCompat; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.ktx.ViewUtils; import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.util.PicassoHelper; +import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.views.AnimatedProgressBar; import java.time.format.DateTimeFormatter; @@ -59,7 +59,7 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder { itemVideoTitleView.setText(item.getStreamEntity().getTitle()); itemAdditionalDetailsView.setText(Localization .concatenateStrings(item.getStreamEntity().getUploader(), - NewPipe.getNameOfService(item.getStreamEntity().getServiceId()))); + ServiceHelper.getNameOfServiceById(item.getStreamEntity().getServiceId()))); if (item.getStreamEntity().getDuration() > 0) { itemDurationView.setText(Localization diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java index d2fe8b40f..0d88eecba 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java @@ -11,12 +11,12 @@ import androidx.core.content.ContextCompat; import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.ktx.ViewUtils; import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.util.PicassoHelper; +import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.views.AnimatedProgressBar; import java.time.format.DateTimeFormatter; @@ -70,11 +70,12 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder { private String getStreamInfoDetailLine(final StreamStatisticsEntry entry, final DateTimeFormatter dateTimeFormatter) { - final String watchCount = Localization - .shortViewCount(itemBuilder.getContext(), entry.getWatchCount()); - final String uploadDate = dateTimeFormatter.format(entry.getLatestAccessDate()); - final String serviceName = NewPipe.getNameOfService(entry.getStreamEntity().getServiceId()); - return Localization.concatenateStrings(watchCount, uploadDate, serviceName); + return Localization.concatenateStrings( + // watchCount + Localization.shortViewCount(itemBuilder.getContext(), entry.getWatchCount()), + dateTimeFormatter.format(entry.getLatestAccessDate()), + // serviceName + ServiceHelper.getNameOfServiceById(entry.getStreamEntity().getServiceId())); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java index 440353ac7..70987a6fc 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java @@ -5,11 +5,11 @@ import android.view.ViewGroup; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.history.HistoryRecordManager; -import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.Localization; +import org.schabi.newpipe.util.PicassoHelper; +import org.schabi.newpipe.util.ServiceHelper; import java.time.format.DateTimeFormatter; @@ -39,9 +39,9 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder { // Here is where the uploader name is set in the bookmarked playlists library if (!TextUtils.isEmpty(item.getUploader())) { itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(), - NewPipe.getNameOfService(item.getServiceId()))); + ServiceHelper.getNameOfServiceById(item.getServiceId()))); } else { - itemUploaderView.setText(NewPipe.getNameOfService(item.getServiceId())); + itemUploaderView.setText(ServiceHelper.getNameOfServiceById(item.getServiceId())); } PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView); diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index 0eb56d716..6023d4b10 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -419,9 +419,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment() { private lateinit var subscriptionManager: SubscriptionManager private val disposables: CompositeDisposable = CompositeDisposable() - private var subscriptionBroadcastReceiver: BroadcastReceiver? = null - private val groupAdapter = GroupAdapter>() private val feedGroupsSection = Section() private var feedGroupsCarousel: FeedGroupCarouselItem? = null - private lateinit var importExportItem: FeedImportExportItem private lateinit var feedGroupsSortMenuItem: HeaderWithMenuItem private val subscriptionsSection = Section() @@ -91,12 +87,10 @@ class SubscriptionFragment : BaseStateFragment() { @State @JvmField var itemsListState: Parcelable? = null + @State @JvmField var feedGroupsListState: Parcelable? = null - @State - @JvmField - var importExportItemExpandedState: Boolean? = null init { setHasOptionsMenu(true) @@ -120,20 +114,10 @@ class SubscriptionFragment : BaseStateFragment() { return inflater.inflate(R.layout.fragment_subscription, container, false) } - override fun onResume() { - super.onResume() - setupBroadcastReceiver() - } - override fun onPause() { super.onPause() itemsListState = binding.itemsList.layoutManager?.onSaveInstanceState() feedGroupsListState = feedGroupsCarousel?.onSaveInstanceState() - importExportItemExpandedState = importExportItem.isExpanded - - if (subscriptionBroadcastReceiver != null && activity != null) { - LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver!!) - } } override fun onDestroy() { @@ -150,28 +134,61 @@ class SubscriptionFragment : BaseStateFragment() { activity.supportActionBar?.setDisplayShowTitleEnabled(true) activity.supportActionBar?.setTitle(R.string.tab_subscriptions) + + buildImportExportMenu(menu) } - private fun setupBroadcastReceiver() { - if (activity == null) return + private fun buildImportExportMenu(menu: Menu) { + // -- Import -- + val importSubMenu = menu.addSubMenu(R.string.import_from) - if (subscriptionBroadcastReceiver != null) { - LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver!!) - } + addMenuItemToSubmenu(importSubMenu, R.string.previous_export) { onImportPreviousSelected() } + .setIcon(R.drawable.ic_backup) - val filters = IntentFilter() - filters.addAction(EXPORT_COMPLETE_ACTION) - filters.addAction(IMPORT_COMPLETE_ACTION) - subscriptionBroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - _binding?.itemsList?.post { - importExportItem.isExpanded = false - importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS) - } + for (service in ServiceList.all()) { + val subscriptionExtractor = service.subscriptionExtractor ?: continue + + val supportedSources = subscriptionExtractor.supportedSources + if (supportedSources.isEmpty()) continue + + addMenuItemToSubmenu(importSubMenu, service.serviceInfo.name) { + onImportFromServiceSelected(service.serviceId) } + .setIcon(ServiceHelper.getIcon(service.serviceId)) } - LocalBroadcastManager.getInstance(activity).registerReceiver(subscriptionBroadcastReceiver!!, filters) + // -- Export -- + val exportSubMenu = menu.addSubMenu(R.string.export_to) + + addMenuItemToSubmenu(exportSubMenu, R.string.file) { onExportSelected() } + .setIcon(R.drawable.ic_save) + } + + private fun addMenuItemToSubmenu( + subMenu: SubMenu, + @StringRes title: Int, + onClick: Runnable + ): MenuItem { + return setClickListenerToMenuItem(subMenu.add(title), onClick) + } + + private fun addMenuItemToSubmenu( + subMenu: SubMenu, + title: String, + onClick: Runnable + ): MenuItem { + return setClickListenerToMenuItem(subMenu.add(title), onClick) + } + + private fun setClickListenerToMenuItem( + menuItem: MenuItem, + onClick: Runnable + ): MenuItem { + menuItem.setOnMenuItemClickListener { _ -> + onClick.run() + true + } + return menuItem } private fun onImportFromServiceSelected(serviceId: Int) { @@ -263,13 +280,14 @@ class SubscriptionFragment : BaseStateFragment() { subscriptionsSection.setPlaceholder(EmptyPlaceholderItem()) subscriptionsSection.setHideWhenEmpty(true) - importExportItem = FeedImportExportItem( - { onImportPreviousSelected() }, - { onImportFromServiceSelected(it) }, - { onExportSelected() }, - importExportItemExpandedState ?: false + groupAdapter.add( + Section( + HeaderWithMenuItem( + getString(R.string.tab_subscriptions) + ), + listOf(subscriptionsSection) + ) ) - groupAdapter.add(Section(importExportItem, listOf(subscriptionsSection))) } override fun initViews(rootView: View, savedInstanceState: Bundle?) { @@ -371,13 +389,6 @@ class SubscriptionFragment : BaseStateFragment() { subscriptionsSection.update(result.subscriptions) subscriptionsSection.setHideWhenEmpty(false) - if (result.subscriptions.isEmpty() && importExportItemExpandedState == null) { - binding.itemsList.post { - importExportItem.isExpanded = true - importExportItem.notifyChanged(FeedImportExportItem.REFRESH_EXPANDED_STATUS) - } - } - if (itemsListState != null) { binding.itemsList.layoutManager?.onRestoreInstanceState(itemsListState) itemsListState = null diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java index 4737fa14f..56972b60d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java @@ -1,5 +1,11 @@ package org.schabi.newpipe.local.subscription; +import static org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.ContentSource.CHANNEL_URL; +import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.CHANNEL_URL_MODE; +import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.INPUT_STREAM_MODE; +import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE; +import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE; + import android.app.Activity; import android.content.Intent; import android.os.Bundle; @@ -40,12 +46,6 @@ import java.util.List; import icepick.State; -import static org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.ContentSource.CHANNEL_URL; -import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.CHANNEL_URL_MODE; -import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.INPUT_STREAM_MODE; -import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE; -import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE; - public class SubscriptionsImportFragment extends BaseFragment { @State int currentServiceId = Constants.NO_SERVICE_ID; @@ -89,7 +89,7 @@ public class SubscriptionsImportFragment extends BaseFragment { if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) { ErrorUtil.showSnackbar(activity, new ErrorInfo(new String[]{}, UserAction.SUBSCRIPTION_IMPORT_EXPORT, - NewPipe.getNameOfService(currentServiceId), + ServiceHelper.getNameOfServiceById(currentServiceId), "Service does not support importing subscriptions", R.string.general_error)); activity.finish(); diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt index 851e84f9f..e96328961 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt @@ -1,7 +1,6 @@ package org.schabi.newpipe.local.subscription.dialog import android.app.Dialog -import android.content.res.ColorStateList import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater @@ -9,7 +8,7 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.Toast -import androidx.core.content.ContextCompat +import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.getSystemService import androidx.core.os.bundleOf import androidx.core.view.isGone @@ -127,7 +126,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable { if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { // KitKat doesn't apply container's theme to content - val contrastColor = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.contrastColor)) + val contrastColor = AppCompatResources.getColorStateList(requireContext(), R.color.contrastColor) searchLayoutBinding.toolbarSearchEditText.setTextColor(contrastColor) searchLayoutBinding.toolbarSearchEditText.setHintTextColor(contrastColor.withAlpha(128)) ImageViewCompat.setImageTintList(searchLayoutBinding.toolbarSearchClearIcon, contrastColor) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedImportExportItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedImportExportItem.kt deleted file mode 100644 index aacfc77ad..000000000 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedImportExportItem.kt +++ /dev/null @@ -1,122 +0,0 @@ -package org.schabi.newpipe.local.subscription.item - -import android.graphics.Color -import android.graphics.PorterDuff -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.annotation.DrawableRes -import com.xwray.groupie.viewbinding.BindableItem -import com.xwray.groupie.viewbinding.GroupieViewHolder -import org.schabi.newpipe.R -import org.schabi.newpipe.databinding.FeedImportExportGroupBinding -import org.schabi.newpipe.extractor.NewPipe -import org.schabi.newpipe.extractor.exceptions.ExtractionException -import org.schabi.newpipe.ktx.animateRotation -import org.schabi.newpipe.util.ServiceHelper -import org.schabi.newpipe.util.ThemeHelper -import org.schabi.newpipe.views.CollapsibleView - -class FeedImportExportItem( - val onImportPreviousSelected: () -> Unit, - val onImportFromServiceSelected: (Int) -> Unit, - val onExportSelected: () -> Unit, - var isExpanded: Boolean = false -) : BindableItem() { - companion object { - const val REFRESH_EXPANDED_STATUS = 123 - } - - override fun bind(viewBinding: FeedImportExportGroupBinding, position: Int, payloads: MutableList) { - if (payloads.contains(REFRESH_EXPANDED_STATUS)) { - viewBinding.importExportOptions.apply { if (isExpanded) expand() else collapse() } - return - } - - super.bind(viewBinding, position, payloads) - } - - override fun getLayout(): Int = R.layout.feed_import_export_group - - override fun bind(viewBinding: FeedImportExportGroupBinding, position: Int) { - if (viewBinding.importFromOptions.childCount == 0) setupImportFromItems(viewBinding.importFromOptions) - if (viewBinding.exportToOptions.childCount == 0) setupExportToItems(viewBinding.exportToOptions) - - expandIconListener?.let { viewBinding.importExportOptions.removeListener(it) } - expandIconListener = CollapsibleView.StateListener { newState -> - viewBinding.importExportExpandIcon.animateRotation( - 250, if (newState == CollapsibleView.COLLAPSED) 0 else 180 - ) - } - - viewBinding.importExportOptions.currentState = if (isExpanded) CollapsibleView.EXPANDED else CollapsibleView.COLLAPSED - viewBinding.importExportExpandIcon.rotation = if (isExpanded) 180F else 0F - viewBinding.importExportOptions.ready() - - viewBinding.importExportOptions.addListener(expandIconListener) - viewBinding.importExport.setOnClickListener { - viewBinding.importExportOptions.switchState() - isExpanded = viewBinding.importExportOptions.currentState == CollapsibleView.EXPANDED - } - } - - override fun unbind(viewHolder: GroupieViewHolder) { - super.unbind(viewHolder) - expandIconListener?.let { viewHolder.binding.importExportOptions.removeListener(it) } - expandIconListener = null - } - - override fun initializeViewBinding(view: View) = FeedImportExportGroupBinding.bind(view) - - private var expandIconListener: CollapsibleView.StateListener? = null - - private fun addItemView(title: String, @DrawableRes icon: Int, container: ViewGroup): View { - val itemRoot = View.inflate(container.context, R.layout.subscription_import_export_item, null) - val titleView = itemRoot.findViewById(android.R.id.text1) - val iconView = itemRoot.findViewById(android.R.id.icon1) - - titleView.text = title - iconView.setImageResource(icon) - - container.addView(itemRoot) - return itemRoot - } - - private fun setupImportFromItems(listHolder: ViewGroup) { - val previousBackupItem = addItemView( - listHolder.context.getString(R.string.previous_export), - R.drawable.ic_backup, listHolder - ) - previousBackupItem.setOnClickListener { onImportPreviousSelected() } - - val iconColor = if (ThemeHelper.isLightThemeSelected(listHolder.context)) Color.BLACK else Color.WHITE - val services = listHolder.context.resources.getStringArray(R.array.service_list) - for (serviceName in services) { - try { - val service = NewPipe.getService(serviceName) - - val subscriptionExtractor = service.subscriptionExtractor ?: continue - - val supportedSources = subscriptionExtractor.supportedSources - if (supportedSources.isEmpty()) continue - - val itemView = addItemView(serviceName, ServiceHelper.getIcon(service.serviceId), listHolder) - val iconView = itemView.findViewById(android.R.id.icon1) - iconView.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN) - - itemView.setOnClickListener { onImportFromServiceSelected(service.serviceId) } - } catch (e: ExtractionException) { - throw RuntimeException("Services array contains an entry that it's not a valid service name ($serviceName)", e) - } - } - } - - private fun setupExportToItems(listHolder: ViewGroup) { - val previousBackupItem = addItemView( - listHolder.context.getString(R.string.file), - R.drawable.ic_save, listHolder - ) - previousBackupItem.setOnClickListener { onExportSelected() } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java index 16679fd4a..a614c5ebe 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java @@ -102,7 +102,10 @@ public final class PlayQueueActivity extends AppCompatActivity getMenuInflater().inflate(R.menu.menu_play_queue, m); getMenuInflater().inflate(R.menu.menu_play_queue_bg, m); onMaybeMuteChanged(); - onPlaybackParameterChanged(player.getPlaybackParameters()); + // to avoid null reference + if (player != null) { + onPlaybackParameterChanged(player.getPlaybackParameters()); + } return true; } diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 1b62ee208..9bdf3a93a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -88,6 +88,7 @@ import android.provider.Settings; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; +import android.view.GestureDetector; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -119,7 +120,6 @@ import androidx.appcompat.widget.PopupMenu; import androidx.collection.ArraySet; import androidx.core.content.ContextCompat; import androidx.core.graphics.Insets; -import androidx.core.view.GestureDetectorCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.FragmentManager; @@ -152,7 +152,6 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; -import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.model.StreamEntity; @@ -396,7 +395,7 @@ public final class Player implements private static final float MAX_GESTURE_LENGTH = 0.75f; private int maxGestureLength; // scaled - private GestureDetectorCompat gestureDetector; + private GestureDetector gestureDetector; private PlayerGestureListener playerGestureListener; /*////////////////////////////////////////////////////////////////////////// @@ -444,7 +443,7 @@ public final class Player implements setupBroadcastReceiver(); trackSelector = new DefaultTrackSelector(context, PlayerHelper.getQualitySelector()); - final PlayerDataSource dataSource = new PlayerDataSource(context, DownloaderImpl.USER_AGENT, + final PlayerDataSource dataSource = new PlayerDataSource(context, new DefaultBandwidthMeter.Builder(context).build()); loadController = new LoadController(); renderFactory = new DefaultRenderersFactory(context); @@ -570,7 +569,7 @@ public final class Player implements binding.playbackLiveSync.setOnClickListener(this); playerGestureListener = new PlayerGestureListener(this, service); - gestureDetector = new GestureDetectorCompat(context, playerGestureListener); + gestureDetector = new GestureDetector(context, playerGestureListener); binding.getRoot().setOnTouchListener(playerGestureListener); binding.queueButton.setOnClickListener(v -> onQueueClicked()); @@ -1774,25 +1773,13 @@ public final class Player implements if (exoPlayerIsNull()) { return; } - // Use duration of currentItem for non-live streams, - // because HLS streams are fragmented - // and thus the whole duration is not available to the player - // TODO: revert #6307 when introducing proper HLS support - final int duration; - if (currentItem != null - && !StreamTypeUtil.isLiveStream(currentItem.getStreamType()) - ) { - // convert seconds to milliseconds - duration = (int) (currentItem.getDuration() * 1000); - } else { - duration = (int) simpleExoPlayer.getDuration(); - } + final int currentProgress = Math.max((int) simpleExoPlayer.getCurrentPosition(), 0); + onUpdateProgress( currentProgress, - duration, - simpleExoPlayer.getBufferedPercentage() - ); + (int) simpleExoPlayer.getDuration(), + simpleExoPlayer.getBufferedPercentage()); if (sponsorBlockMode == SponsorBlockMode.ENABLED && isPrepared) { final VideoSegment segment = getSkippableSegment(currentProgress); @@ -2603,22 +2590,31 @@ public final class Player implements Listener.super.onEvents(player, events); MediaItemTag.from(player.getCurrentMediaItem()).ifPresent(tag -> { if (tag == currentMetadata) { - return; + return; // we still have the same metadata, no need to do anything } + final StreamInfo previousInfo = Optional.ofNullable(currentMetadata) + .flatMap(MediaItemTag::getMaybeStreamInfo).orElse(null); currentMetadata = tag; - if (!tag.getErrors().isEmpty()) { + + if (!currentMetadata.getErrors().isEmpty()) { + // new errors might have been added even if previousInfo == tag.getMaybeStreamInfo() final ErrorInfo errorInfo = new ErrorInfo( - tag.getErrors().get(0), + currentMetadata.getErrors(), UserAction.PLAY_STREAM, - "Loading failed for [" + tag.getTitle() + "]: " + tag.getStreamUrl(), - tag.getServiceId()); + "Loading failed for [" + currentMetadata.getTitle() + + "]: " + currentMetadata.getStreamUrl(), + currentMetadata.getServiceId()); ErrorUtil.createNotification(context, errorInfo); } - tag.getMaybeStreamInfo().ifPresent(info -> { + + currentMetadata.getMaybeStreamInfo().ifPresent(info -> { if (DEBUG) { Log.d(TAG, "ExoPlayer - onEvents() update stream info: " + info.getName()); } - updateMetadataWith(info); + if (previousInfo == null || !previousInfo.getUrl().equals(info.getUrl())) { + // only update with the new stream info if it has actually changed + updateMetadataWith(info); + } }); }); } @@ -3516,6 +3512,7 @@ public final class Player implements switch (info.getStreamType()) { case AUDIO_STREAM: + case POST_LIVE_AUDIO_STREAM: binding.surfaceView.setVisibility(View.GONE); binding.endScreen.setVisibility(View.VISIBLE); binding.playbackEndTime.setVisibility(View.VISIBLE); @@ -3534,6 +3531,7 @@ public final class Player implements break; case VIDEO_STREAM: + case POST_LIVE_STREAM: if (currentMetadata == null || !currentMetadata.getMaybeQuality().isPresent() || (info.getVideoStreams().isEmpty() @@ -3601,10 +3599,10 @@ public final class Player implements for (int i = 0; i < availableStreams.size(); i++) { final VideoStream videoStream = availableStreams.get(i); qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat - .getNameById(videoStream.getFormatId()) + " " + videoStream.resolution); + .getNameById(videoStream.getFormatId()) + " " + videoStream.getResolution()); } if (getSelectedVideoStream() != null) { - binding.qualityTextView.setText(getSelectedVideoStream().resolution); + binding.qualityTextView.setText(getSelectedVideoStream().getResolution()); } qualityPopupMenu.setOnMenuItemClickListener(this); qualityPopupMenu.setOnDismissListener(this); @@ -3722,7 +3720,7 @@ public final class Player implements } saveStreamProgressState(); //TODO added, check if good - final String newResolution = availableStreams.get(menuItemIndex).resolution; + final String newResolution = availableStreams.get(menuItemIndex).getResolution(); setRecovery(); setPlaybackQuality(newResolution); reloadPlayQueueManager(); @@ -3750,7 +3748,7 @@ public final class Player implements } isSomePopupMenuVisible = false; //TODO check if this works if (getSelectedVideoStream() != null) { - binding.qualityTextView.setText(getSelectedVideoStream().resolution); + binding.qualityTextView.setText(getSelectedVideoStream().getResolution()); } if (isPlaying()) { hideControls(DEFAULT_CONTROLS_DURATION, 0); @@ -4401,9 +4399,7 @@ public final class Player implements if (playQueueManagerReloadingNeeded(sourceType, info, getVideoRendererIndex())) { reloadPlayQueueManager(); } else { - final StreamType streamType = info.getStreamType(); - if (streamType == StreamType.AUDIO_STREAM - || streamType == StreamType.AUDIO_LIVE_STREAM) { + if (StreamTypeUtil.isAudio(info.getStreamType())) { // Nothing to do more than setting the recovery position setRecovery(); return; @@ -4438,13 +4434,15 @@ public final class Player implements * the content is not an audio content, but also if none of the following cases is met: * *
    - *
  • the content is an {@link StreamType#AUDIO_STREAM audio stream} or an - * {@link StreamType#AUDIO_LIVE_STREAM audio live stream};
  • + *
  • the content is an {@link StreamType#AUDIO_STREAM audio stream}, an + * {@link StreamType#AUDIO_LIVE_STREAM audio live stream}, or a + * {@link StreamType#POST_LIVE_AUDIO_STREAM ended audio live stream};
  • *
  • the content is a {@link StreamType#LIVE_STREAM live stream} and the source type is a * {@link SourceType#LIVE_STREAM live source};
  • *
  • the content's source is {@link SourceType#VIDEO_WITH_SEPARATED_AUDIO a video stream * with a separated audio source} or has no audio-only streams available and is a - * {@link StreamType#LIVE_STREAM live stream} or a + * {@link StreamType#VIDEO_STREAM video stream}, an + * {@link StreamType#POST_LIVE_STREAM ended live stream}, or a * {@link StreamType#LIVE_STREAM live stream}. *
  • *
@@ -4460,18 +4458,17 @@ public final class Player implements @NonNull final StreamInfo streamInfo, final int videoRendererIndex) { final StreamType streamType = streamInfo.getStreamType(); + final boolean isStreamTypeAudio = StreamTypeUtil.isAudio(streamType); - if (videoRendererIndex == RENDERER_UNAVAILABLE && streamType != StreamType.AUDIO_STREAM - && streamType != StreamType.AUDIO_LIVE_STREAM) { + if (videoRendererIndex == RENDERER_UNAVAILABLE && !isStreamTypeAudio) { return true; } // The content is an audio stream, an audio live stream, or a live stream with a live // source: it's not needed to reload the play queue manager because the stream source will // be the same - if ((streamType == StreamType.AUDIO_STREAM || streamType == StreamType.AUDIO_LIVE_STREAM) - || (streamType == StreamType.LIVE_STREAM - && sourceType == SourceType.LIVE_STREAM)) { + if (isStreamTypeAudio || (streamType == StreamType.LIVE_STREAM + && sourceType == SourceType.LIVE_STREAM)) { return false; } @@ -4484,8 +4481,8 @@ public final class Player implements || (sourceType == SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY && isNullOrEmpty(streamInfo.getAudioStreams()))) { // It's not needed to reload the play queue manager only if the content's stream type - // is a video stream or a live stream - return streamType != StreamType.VIDEO_STREAM && streamType != StreamType.LIVE_STREAM; + // is a video stream, a live stream or an ended live stream + return !StreamTypeUtil.isVideo(streamType); } // Other cases: the play queue manager reload is needed @@ -4581,7 +4578,7 @@ public final class Player implements return audioReactor; } - public GestureDetectorCompat getGestureDetector() { + public GestureDetector getGestureDetector() { return gestureDetector; } diff --git a/app/src/main/java/org/schabi/newpipe/player/datasource/NonUriHlsDataSourceFactory.java b/app/src/main/java/org/schabi/newpipe/player/datasource/NonUriHlsDataSourceFactory.java new file mode 100644 index 000000000..676443a9c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/datasource/NonUriHlsDataSourceFactory.java @@ -0,0 +1,136 @@ +package org.schabi.newpipe.player.datasource; + +import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; + +import androidx.annotation.NonNull; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; +import com.google.android.exoplayer2.upstream.ByteArrayDataSource; +import com.google.android.exoplayer2.upstream.DataSource; + +import java.nio.charset.StandardCharsets; + +/** + * A {@link HlsDataSourceFactory} which allows playback of non-URI media HLS playlists for + * {@link com.google.android.exoplayer2.source.hls.HlsMediaSource HlsMediaSource}s. + * + *

+ * If media requests are relative, the URI from which the manifest comes from (either the + * manifest URI (preferred) or the master URI (if applicable)) must be returned, otherwise the + * content will be not playable, as it will be an invalid URL, or it may be treat as something + * unexpected, for instance as a file for + * {@link com.google.android.exoplayer2.upstream.DefaultDataSource DefaultDataSource}s. + *

+ * + *

+ * See {@link #createDataSource(int)} for changes and implementation details. + *

+ */ +public final class NonUriHlsDataSourceFactory implements HlsDataSourceFactory { + + /** + * Builder class of {@link NonUriHlsDataSourceFactory} instances. + */ + public static final class Builder { + private DataSource.Factory dataSourceFactory; + private String playlistString; + + /** + * Set the {@link DataSource.Factory} which will be used to create non manifest contents + * {@link DataSource}s. + * + * @param dataSourceFactoryForNonManifestContents the {@link DataSource.Factory} which will + * be used to create non manifest contents + * {@link DataSource}s, which cannot be null + */ + public void setDataSourceFactory( + @NonNull final DataSource.Factory dataSourceFactoryForNonManifestContents) { + this.dataSourceFactory = dataSourceFactoryForNonManifestContents; + } + + /** + * Set the HLS playlist which will be used for manifests requests. + * + * @param hlsPlaylistString the string which correspond to the response of the HLS + * manifest, which cannot be null or empty + */ + public void setPlaylistString(@NonNull final String hlsPlaylistString) { + this.playlistString = hlsPlaylistString; + } + + /** + * Create a new {@link NonUriHlsDataSourceFactory} with the given data source factory and + * the given HLS playlist. + * + * @return a {@link NonUriHlsDataSourceFactory} + * @throws IllegalArgumentException if the data source factory is null or if the HLS + * playlist string set is null or empty + */ + @NonNull + public NonUriHlsDataSourceFactory build() { + if (dataSourceFactory == null) { + throw new IllegalArgumentException( + "No DataSource.Factory valid instance has been specified."); + } + + if (isNullOrEmpty(playlistString)) { + throw new IllegalArgumentException("No HLS valid playlist has been specified."); + } + + return new NonUriHlsDataSourceFactory(dataSourceFactory, + playlistString.getBytes(StandardCharsets.UTF_8)); + } + } + + private final DataSource.Factory dataSourceFactory; + private final byte[] playlistStringByteArray; + + /** + * Create a {@link NonUriHlsDataSourceFactory} instance. + * + * @param dataSourceFactory the {@link DataSource.Factory} which will be used to build + * non manifests {@link DataSource}s, which must not be null + * @param playlistStringByteArray a byte array of the HLS playlist, which must not be null + */ + private NonUriHlsDataSourceFactory(@NonNull final DataSource.Factory dataSourceFactory, + @NonNull final byte[] playlistStringByteArray) { + this.dataSourceFactory = dataSourceFactory; + this.playlistStringByteArray = playlistStringByteArray; + } + + /** + * Create a {@link DataSource} for the given data type. + * + *

+ * Contrary to {@link com.google.android.exoplayer2.source.hls.DefaultHlsDataSourceFactory + * ExoPlayer's default implementation}, this implementation is not always using the + * {@link DataSource.Factory} passed to the + * {@link com.google.android.exoplayer2.source.hls.HlsMediaSource.Factory + * HlsMediaSource.Factory} constructor, only when it's not + * {@link C#DATA_TYPE_MANIFEST the manifest type}. + *

+ * + *

+ * This change allow playback of non-URI HLS contents, when the manifest is not a master + * manifest/playlist (otherwise, endless loops should be encountered because the + * {@link DataSource}s created for media playlists should use the master playlist response + * instead). + *

+ * + * @param dataType the data type for which the {@link DataSource} will be used, which is one of + * {@link C} {@code .DATA_TYPE_*} constants + * @return a {@link DataSource} for the given data type + */ + @NonNull + @Override + public DataSource createDataSource(final int dataType) { + // The manifest is already downloaded and provided with playlistStringByteArray, so we + // don't need to download it again and we can use a ByteArrayDataSource instead + if (dataType == C.DATA_TYPE_MANIFEST) { + return new ByteArrayDataSource(playlistStringByteArray); + } + + return dataSourceFactory.createDataSource(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java b/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java new file mode 100644 index 000000000..c9abe65f6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java @@ -0,0 +1,1014 @@ +/* + * Based on ExoPlayer's DefaultHttpDataSource, version 2.17.1. + * + * Original source code copyright (C) 2016 The Android Open Source Project, licensed under the + * Apache License, Version 2.0. + */ + +package org.schabi.newpipe.player.datasource; + +import static com.google.android.exoplayer2.upstream.DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS; +import static com.google.android.exoplayer2.upstream.DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS; +import static com.google.android.exoplayer2.upstream.HttpUtil.buildRangeRequestHeader; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Util.castNonNull; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getAndroidUserAgent; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getIosUserAgent; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isAndroidStreamingUrl; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isIosStreamingUrl; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebStreamingUrl; +import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isTvHtml5SimplyEmbeddedPlayerStreamingUrl; +import static java.lang.Math.min; + +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.PlaybackException; +import com.google.android.exoplayer2.upstream.BaseDataSource; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSourceException; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; +import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.HttpUtil; +import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.Util; +import com.google.common.base.Predicate; +import com.google.common.collect.ForwardingMap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; +import com.google.common.net.HttpHeaders; + +import org.schabi.newpipe.DownloaderImpl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.lang.reflect.Method; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.NoRouteToHostException; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.zip.GZIPInputStream; + +/** + * An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}, based on + * {@link com.google.android.exoplayer2.upstream.DefaultHttpDataSource}, for YouTube streams. + * + *

+ * It adds more headers to {@code videoplayback} URLs, such as {@code Origin}, {@code Referer} + * (only where it's relevant) and also more parameters, such as {@code rn} and replaces the use of + * the {@code Range} header by the corresponding parameter ({@code range}), if enabled. + *

+ * + * There are many unused methods in this class because everything was copied from {@link + * com.google.android.exoplayer2.upstream.DefaultHttpDataSource} with as little changes as possible. + * SonarQube warnings were also suppressed for the same reason. + */ +@SuppressWarnings({"squid:S3011", "squid:S4738"}) +public final class YoutubeHttpDataSource extends BaseDataSource implements HttpDataSource { + + /** + * {@link DataSource.Factory} for {@link YoutubeHttpDataSource} instances. + */ + public static final class Factory implements HttpDataSource.Factory { + + private final RequestProperties defaultRequestProperties; + + @Nullable + private TransferListener transferListener; + @Nullable + private Predicate contentTypePredicate; + private int connectTimeoutMs; + private int readTimeoutMs; + private boolean allowCrossProtocolRedirects; + private boolean keepPostFor302Redirects; + + private boolean rangeParameterEnabled; + private boolean rnParameterEnabled; + + /** + * Creates an instance. + */ + public Factory() { + defaultRequestProperties = new RequestProperties(); + connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MILLIS; + readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS; + } + + @NonNull + @Override + public Factory setDefaultRequestProperties( + @NonNull final Map defaultRequestPropertiesMap) { + defaultRequestProperties.clearAndSet(defaultRequestPropertiesMap); + return this; + } + + /** + * Sets the connect timeout, in milliseconds. + * + *

+ * The default is {@link DefaultHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS}. + *

+ * + * @param connectTimeoutMsValue The connect timeout, in milliseconds, that will be used. + * @return This factory. + */ + public Factory setConnectTimeoutMs(final int connectTimeoutMsValue) { + connectTimeoutMs = connectTimeoutMsValue; + return this; + } + + /** + * Sets the read timeout, in milliseconds. + * + *

The default is {@link DefaultHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS}. + * + * @param readTimeoutMsValue The connect timeout, in milliseconds, that will be used. + * @return This factory. + */ + public Factory setReadTimeoutMs(final int readTimeoutMsValue) { + readTimeoutMs = readTimeoutMsValue; + return this; + } + + /** + * Sets whether to allow cross protocol redirects. + * + *

The default is {@code false}. + * + * @param allowCrossProtocolRedirectsValue Whether to allow cross protocol redirects. + * @return This factory. + */ + public Factory setAllowCrossProtocolRedirects( + final boolean allowCrossProtocolRedirectsValue) { + allowCrossProtocolRedirects = allowCrossProtocolRedirectsValue; + return this; + } + + /** + * Sets whether the use of the {@code range} parameter instead of the {@code Range} header + * to request ranges of streams is enabled. + * + *

+ * Note that it must be not enabled on streams which are using a {@link + * com.google.android.exoplayer2.source.ProgressiveMediaSource}, as it will break playback + * for them (some exceptions may be thrown). + *

+ * + * @param rangeParameterEnabledValue whether the use of the {@code range} parameter instead + * of the {@code Range} header (must be only enabled when + * non-{@code ProgressiveMediaSource}s) + * @return This factory. + */ + public Factory setRangeParameterEnabled(final boolean rangeParameterEnabledValue) { + rangeParameterEnabled = rangeParameterEnabledValue; + return this; + } + + /** + * Sets whether the use of the {@code rn}, which stands for request number, parameter is + * enabled. + * + *

+ * Note that it should be not enabled on streams which are using {@code /} to delimit URLs + * parameters, such as the streams of HLS manifests. + *

+ * + * @param rnParameterEnabledValue whether the appending the {@code rn} parameter to + * {@code videoplayback} URLs + * @return This factory. + */ + public Factory setRnParameterEnabled(final boolean rnParameterEnabledValue) { + rnParameterEnabled = rnParameterEnabledValue; + return this; + } + + /** + * Sets a content type {@link Predicate}. If a content type is rejected by the predicate + * then a {@link HttpDataSource.InvalidContentTypeException} is thrown from + * {@link YoutubeHttpDataSource#open(DataSpec)}. + * + *

+ * The default is {@code null}. + *

+ * + * @param contentTypePredicateToSet The content type {@link Predicate}, or {@code null} to + * clear a predicate that was previously set. + * @return This factory. + */ + public Factory setContentTypePredicate( + @Nullable final Predicate contentTypePredicateToSet) { + this.contentTypePredicate = contentTypePredicateToSet; + return this; + } + + /** + * Sets the {@link TransferListener} that will be used. + * + *

The default is {@code null}. + * + *

See {@link DataSource#addTransferListener(TransferListener)}. + * + * @param transferListenerToUse The listener that will be used. + * @return This factory. + */ + public Factory setTransferListener( + @Nullable final TransferListener transferListenerToUse) { + this.transferListener = transferListenerToUse; + return this; + } + + /** + * Sets whether we should keep the POST method and body when we have HTTP 302 redirects for + * a POST request. + * + * @param keepPostFor302RedirectsValue Whether we should keep the POST method and body when + * we have HTTP 302 redirects for a POST request. + * @return This factory. + */ + public Factory setKeepPostFor302Redirects(final boolean keepPostFor302RedirectsValue) { + this.keepPostFor302Redirects = keepPostFor302RedirectsValue; + return this; + } + + @NonNull + @Override + public YoutubeHttpDataSource createDataSource() { + final YoutubeHttpDataSource dataSource = new YoutubeHttpDataSource( + connectTimeoutMs, + readTimeoutMs, + allowCrossProtocolRedirects, + rangeParameterEnabled, + rnParameterEnabled, + defaultRequestProperties, + contentTypePredicate, + keepPostFor302Redirects); + if (transferListener != null) { + dataSource.addTransferListener(transferListener); + } + return dataSource; + } + } + + private static final String TAG = YoutubeHttpDataSource.class.getSimpleName(); + private static final int MAX_REDIRECTS = 20; // Same limit as okhttp. + private static final int HTTP_STATUS_TEMPORARY_REDIRECT = 307; + private static final int HTTP_STATUS_PERMANENT_REDIRECT = 308; + private static final long MAX_BYTES_TO_DRAIN = 2048; + + private static final String RN_PARAMETER = "&rn="; + private static final String YOUTUBE_BASE_URL = "https://www.youtube.com"; + + private final boolean allowCrossProtocolRedirects; + private final boolean rangeParameterEnabled; + private final boolean rnParameterEnabled; + + private final int connectTimeoutMillis; + private final int readTimeoutMillis; + @Nullable + private final RequestProperties defaultRequestProperties; + private final RequestProperties requestProperties; + private final boolean keepPostFor302Redirects; + + @Nullable + private final Predicate contentTypePredicate; + @Nullable + private DataSpec dataSpec; + @Nullable + private HttpURLConnection connection; + @Nullable + private InputStream inputStream; + private boolean opened; + private int responseCode; + private long bytesToRead; + private long bytesRead; + + private long requestNumber; + + @SuppressWarnings("checkstyle:ParameterNumber") + private YoutubeHttpDataSource(final int connectTimeoutMillis, + final int readTimeoutMillis, + final boolean allowCrossProtocolRedirects, + final boolean rangeParameterEnabled, + final boolean rnParameterEnabled, + @Nullable final RequestProperties defaultRequestProperties, + @Nullable final Predicate contentTypePredicate, + final boolean keepPostFor302Redirects) { + super(true); + this.connectTimeoutMillis = connectTimeoutMillis; + this.readTimeoutMillis = readTimeoutMillis; + this.allowCrossProtocolRedirects = allowCrossProtocolRedirects; + this.rangeParameterEnabled = rangeParameterEnabled; + this.rnParameterEnabled = rnParameterEnabled; + this.defaultRequestProperties = defaultRequestProperties; + this.contentTypePredicate = contentTypePredicate; + this.requestProperties = new RequestProperties(); + this.keepPostFor302Redirects = keepPostFor302Redirects; + this.requestNumber = 0; + } + + @Override + @Nullable + public Uri getUri() { + return connection == null ? null : Uri.parse(connection.getURL().toString()); + } + + @Override + public int getResponseCode() { + return connection == null || responseCode <= 0 ? -1 : responseCode; + } + + @NonNull + @Override + public Map> getResponseHeaders() { + if (connection == null) { + return ImmutableMap.of(); + } + // connection.getHeaderFields() always contains a null key with a value like + // ["HTTP/1.1 200 OK"]. The response code is available from + // HttpURLConnection#getResponseCode() and the HTTP version is fixed when establishing the + // connection. + // DataSource#getResponseHeaders() doesn't allow null keys in the returned map, so we need + // to remove it. + // connection.getHeaderFields() returns a special unmodifiable case-insensitive Map + // so we can't just remove the null key or make a copy without the null key. Instead we + // wrap it in a ForwardingMap subclass that ignores and filters out null keys in the read + // methods. + return new NullFilteringHeadersMap(connection.getHeaderFields()); + } + + @Override + public void setRequestProperty(@NonNull final String name, @NonNull final String value) { + checkNotNull(name); + checkNotNull(value); + requestProperties.set(name, value); + } + + @Override + public void clearRequestProperty(@NonNull final String name) { + checkNotNull(name); + requestProperties.remove(name); + } + + @Override + public void clearAllRequestProperties() { + requestProperties.clear(); + } + + /** + * Opens the source to read the specified data. + */ + @Override + public long open(@NonNull final DataSpec dataSpecParameter) throws HttpDataSourceException { + this.dataSpec = dataSpecParameter; + bytesRead = 0; + bytesToRead = 0; + transferInitializing(dataSpecParameter); + + final HttpURLConnection httpURLConnection; + final String responseMessage; + try { + this.connection = makeConnection(dataSpec); + httpURLConnection = this.connection; + responseCode = httpURLConnection.getResponseCode(); + responseMessage = httpURLConnection.getResponseMessage(); + } catch (final IOException e) { + closeConnectionQuietly(); + throw HttpDataSourceException.createForIOException(e, dataSpec, + HttpDataSourceException.TYPE_OPEN); + } + + // Check for a valid response code. + if (responseCode < 200 || responseCode > 299) { + final Map> headers = httpURLConnection.getHeaderFields(); + if (responseCode == 416) { + final long documentSize = HttpUtil.getDocumentSize( + httpURLConnection.getHeaderField(HttpHeaders.CONTENT_RANGE)); + if (dataSpecParameter.position == documentSize) { + opened = true; + transferStarted(dataSpecParameter); + return dataSpecParameter.length != C.LENGTH_UNSET + ? dataSpecParameter.length + : 0; + } + } + + final InputStream errorStream = httpURLConnection.getErrorStream(); + byte[] errorResponseBody; + try { + errorResponseBody = errorStream != null + ? Util.toByteArray(errorStream) + : Util.EMPTY_BYTE_ARRAY; + } catch (final IOException e) { + errorResponseBody = Util.EMPTY_BYTE_ARRAY; + } + + closeConnectionQuietly(); + final IOException cause = responseCode == 416 ? new DataSourceException( + PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE) + : null; + throw new InvalidResponseCodeException(responseCode, responseMessage, cause, headers, + dataSpec, errorResponseBody); + } + + // Check for a valid content type. + final String contentType = httpURLConnection.getContentType(); + if (contentTypePredicate != null && !contentTypePredicate.apply(contentType)) { + closeConnectionQuietly(); + throw new InvalidContentTypeException(contentType, dataSpecParameter); + } + + final long bytesToSkip; + if (!rangeParameterEnabled) { + // If we requested a range starting from a non-zero position and received a 200 rather + // than a 206, then the server does not support partial requests. We'll need to + // manually skip to the requested position. + bytesToSkip = responseCode == 200 && dataSpecParameter.position != 0 + ? dataSpecParameter.position + : 0; + } else { + bytesToSkip = 0; + } + + + // Determine the length of the data to be read, after skipping. + final boolean isCompressed = isCompressed(httpURLConnection); + if (!isCompressed) { + if (dataSpecParameter.length != C.LENGTH_UNSET) { + bytesToRead = dataSpecParameter.length; + } else { + final long contentLength = HttpUtil.getContentLength( + httpURLConnection.getHeaderField(HttpHeaders.CONTENT_LENGTH), + httpURLConnection.getHeaderField(HttpHeaders.CONTENT_RANGE)); + bytesToRead = contentLength != C.LENGTH_UNSET + ? (contentLength - bytesToSkip) + : C.LENGTH_UNSET; + } + } else { + // Gzip is enabled. If the server opts to use gzip then the content length in the + // response will be that of the compressed data, which isn't what we want. Always use + // the dataSpec length in this case. + bytesToRead = dataSpecParameter.length; + } + + try { + inputStream = httpURLConnection.getInputStream(); + if (isCompressed) { + inputStream = new GZIPInputStream(inputStream); + } + } catch (final IOException e) { + closeConnectionQuietly(); + throw new HttpDataSourceException(e, dataSpec, + PlaybackException.ERROR_CODE_IO_UNSPECIFIED, + HttpDataSourceException.TYPE_OPEN); + } + + opened = true; + transferStarted(dataSpecParameter); + + try { + skipFully(bytesToSkip, dataSpec); + } catch (final IOException e) { + closeConnectionQuietly(); + if (e instanceof HttpDataSourceException) { + throw (HttpDataSourceException) e; + } + throw new HttpDataSourceException(e, dataSpec, + PlaybackException.ERROR_CODE_IO_UNSPECIFIED, + HttpDataSourceException.TYPE_OPEN); + } + + return bytesToRead; + } + + @Override + public int read(@NonNull final byte[] buffer, final int offset, final int length) + throws HttpDataSourceException { + try { + return readInternal(buffer, offset, length); + } catch (final IOException e) { + throw HttpDataSourceException.createForIOException(e, castNonNull(dataSpec), + HttpDataSourceException.TYPE_READ); + } + } + + @Override + public void close() throws HttpDataSourceException { + try { + final InputStream connectionInputStream = this.inputStream; + if (connectionInputStream != null) { + final long bytesRemaining = bytesToRead == C.LENGTH_UNSET + ? C.LENGTH_UNSET + : bytesToRead - bytesRead; + maybeTerminateInputStream(connection, bytesRemaining); + + try { + connectionInputStream.close(); + } catch (final IOException e) { + throw new HttpDataSourceException(e, castNonNull(dataSpec), + PlaybackException.ERROR_CODE_IO_UNSPECIFIED, + HttpDataSourceException.TYPE_CLOSE); + } + } + } finally { + inputStream = null; + closeConnectionQuietly(); + if (opened) { + opened = false; + transferEnded(); + } + } + } + + @NonNull + private HttpURLConnection makeConnection(@NonNull final DataSpec dataSpecToUse) + throws IOException { + URL url = new URL(dataSpecToUse.uri.toString()); + @HttpMethod int httpMethod = dataSpecToUse.httpMethod; + @Nullable byte[] httpBody = dataSpecToUse.httpBody; + final long position = dataSpecToUse.position; + final long length = dataSpecToUse.length; + final boolean allowGzip = dataSpecToUse.isFlagSet(DataSpec.FLAG_ALLOW_GZIP); + + if (!allowCrossProtocolRedirects && !keepPostFor302Redirects) { + // HttpURLConnection disallows cross-protocol redirects, but otherwise performs + // redirection automatically. This is the behavior we want, so use it. + return makeConnection(url, httpMethod, httpBody, position, length, allowGzip, true, + dataSpecToUse.httpRequestHeaders); + } + + // We need to handle redirects ourselves to allow cross-protocol redirects or to keep the + // POST request method for 302. + int redirectCount = 0; + while (redirectCount++ <= MAX_REDIRECTS) { + final HttpURLConnection httpURLConnection = makeConnection(url, httpMethod, httpBody, + position, length, allowGzip, false, dataSpecToUse.httpRequestHeaders); + final int httpURLConnectionResponseCode = httpURLConnection.getResponseCode(); + final String location = httpURLConnection.getHeaderField("Location"); + if ((httpMethod == DataSpec.HTTP_METHOD_GET || httpMethod == DataSpec.HTTP_METHOD_HEAD) + && (httpURLConnectionResponseCode == HttpURLConnection.HTTP_MULT_CHOICE + || httpURLConnectionResponseCode == HttpURLConnection.HTTP_MOVED_PERM + || httpURLConnectionResponseCode == HttpURLConnection.HTTP_MOVED_TEMP + || httpURLConnectionResponseCode == HttpURLConnection.HTTP_SEE_OTHER + || httpURLConnectionResponseCode == HTTP_STATUS_TEMPORARY_REDIRECT + || httpURLConnectionResponseCode == HTTP_STATUS_PERMANENT_REDIRECT)) { + httpURLConnection.disconnect(); + url = handleRedirect(url, location, dataSpecToUse); + } else if (httpMethod == DataSpec.HTTP_METHOD_POST + && (httpURLConnectionResponseCode == HttpURLConnection.HTTP_MULT_CHOICE + || httpURLConnectionResponseCode == HttpURLConnection.HTTP_MOVED_PERM + || httpURLConnectionResponseCode == HttpURLConnection.HTTP_MOVED_TEMP + || httpURLConnectionResponseCode == HttpURLConnection.HTTP_SEE_OTHER)) { + httpURLConnection.disconnect(); + final boolean shouldKeepPost = keepPostFor302Redirects + && responseCode == HttpURLConnection.HTTP_MOVED_TEMP; + if (!shouldKeepPost) { + // POST request follows the redirect and is transformed into a GET request. + httpMethod = DataSpec.HTTP_METHOD_GET; + httpBody = null; + } + url = handleRedirect(url, location, dataSpecToUse); + } else { + return httpURLConnection; + } + } + + // If we get here we've been redirected more times than are permitted. + throw new HttpDataSourceException( + new NoRouteToHostException("Too many redirects: " + redirectCount), + dataSpecToUse, + PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, + HttpDataSourceException.TYPE_OPEN); + } + + /** + * Configures a connection and opens it. + * + * @param url The url to connect to. + * @param httpMethod The http method. + * @param httpBody The body data, or {@code null} if not required. + * @param position The byte offset of the requested data. + * @param length The length of the requested data, or {@link C#LENGTH_UNSET}. + * @param allowGzip Whether to allow the use of gzip. + * @param followRedirects Whether to follow redirects. + * @param requestParameters parameters (HTTP headers) to include in request. + * @return the connection opened + */ + @SuppressWarnings("checkstyle:ParameterNumber") + @NonNull + private HttpURLConnection makeConnection( + @NonNull final URL url, + @HttpMethod final int httpMethod, + @Nullable final byte[] httpBody, + final long position, + final long length, + final boolean allowGzip, + final boolean followRedirects, + final Map requestParameters) throws IOException { + // This is the method that contains breaking changes with respect to DefaultHttpDataSource! + + String requestUrl = url.toString(); + + // Don't add the request number parameter if it has been already added (for instance in + // DASH manifests) or if that's not a videoplayback URL + final boolean isVideoPlaybackUrl = url.getPath().startsWith("/videoplayback"); + if (isVideoPlaybackUrl && rnParameterEnabled && !requestUrl.contains(RN_PARAMETER)) { + requestUrl += RN_PARAMETER + requestNumber; + ++requestNumber; + } + + if (rangeParameterEnabled && isVideoPlaybackUrl) { + final String rangeParameterBuilt = buildRangeParameter(position, length); + if (rangeParameterBuilt != null) { + requestUrl += rangeParameterBuilt; + } + } + + final HttpURLConnection httpURLConnection = openConnection(new URL(requestUrl)); + httpURLConnection.setConnectTimeout(connectTimeoutMillis); + httpURLConnection.setReadTimeout(readTimeoutMillis); + + final Map requestHeaders = new HashMap<>(); + if (defaultRequestProperties != null) { + requestHeaders.putAll(defaultRequestProperties.getSnapshot()); + } + requestHeaders.putAll(requestProperties.getSnapshot()); + requestHeaders.putAll(requestParameters); + + for (final Map.Entry property : requestHeaders.entrySet()) { + httpURLConnection.setRequestProperty(property.getKey(), property.getValue()); + } + + if (!rangeParameterEnabled) { + final String rangeHeader = buildRangeRequestHeader(position, length); + if (rangeHeader != null) { + httpURLConnection.setRequestProperty(HttpHeaders.RANGE, rangeHeader); + } + } + + if (isWebStreamingUrl(requestUrl) + || isTvHtml5SimplyEmbeddedPlayerStreamingUrl(requestUrl)) { + httpURLConnection.setRequestProperty(HttpHeaders.ORIGIN, YOUTUBE_BASE_URL); + httpURLConnection.setRequestProperty(HttpHeaders.REFERER, YOUTUBE_BASE_URL); + httpURLConnection.setRequestProperty(HttpHeaders.SEC_FETCH_DEST, "empty"); + httpURLConnection.setRequestProperty(HttpHeaders.SEC_FETCH_MODE, "cors"); + httpURLConnection.setRequestProperty(HttpHeaders.SEC_FETCH_SITE, "cross-site"); + } + + httpURLConnection.setRequestProperty(HttpHeaders.TE, "trailers"); + + final boolean isAndroidStreamingUrl = isAndroidStreamingUrl(requestUrl); + final boolean isIosStreamingUrl = isIosStreamingUrl(requestUrl); + if (isAndroidStreamingUrl) { + // Improvement which may be done: find the content country used to request YouTube + // contents to add it in the user agent instead of using the default + httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT, + getAndroidUserAgent(null)); + } else if (isIosStreamingUrl) { + httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT, + getIosUserAgent(null)); + } else { + // non-mobile user agent + httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT, DownloaderImpl.USER_AGENT); + } + + httpURLConnection.setRequestProperty(HttpHeaders.ACCEPT_ENCODING, + allowGzip ? "gzip" : "identity"); + httpURLConnection.setInstanceFollowRedirects(followRedirects); + httpURLConnection.setDoOutput(httpBody != null); + + // Mobile clients uses POST requests to fetch contents + httpURLConnection.setRequestMethod(isAndroidStreamingUrl || isIosStreamingUrl + ? "POST" + : DataSpec.getStringForHttpMethod(httpMethod)); + + if (httpBody != null) { + httpURLConnection.setFixedLengthStreamingMode(httpBody.length); + httpURLConnection.connect(); + final OutputStream os = httpURLConnection.getOutputStream(); + os.write(httpBody); + os.close(); + } else { + httpURLConnection.connect(); + } + return httpURLConnection; + } + + /** + * Creates an {@link HttpURLConnection} that is connected with the {@code url}. + * + * @param url the {@link URL} to create an {@link HttpURLConnection} + * @return an {@link HttpURLConnection} created with the {@code url} + */ + private HttpURLConnection openConnection(@NonNull final URL url) throws IOException { + return (HttpURLConnection) url.openConnection(); + } + + /** + * Handles a redirect. + * + * @param originalUrl The original URL. + * @param location The Location header in the response. May be {@code null}. + * @param dataSpecToHandleRedirect The {@link DataSpec}. + * @return The next URL. + * @throws HttpDataSourceException If redirection isn't possible. + */ + @NonNull + private URL handleRedirect(final URL originalUrl, + @Nullable final String location, + final DataSpec dataSpecToHandleRedirect) + throws HttpDataSourceException { + if (location == null) { + throw new HttpDataSourceException("Null location redirect", dataSpecToHandleRedirect, + PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, + HttpDataSourceException.TYPE_OPEN); + } + + // Form the new url. + final URL url; + try { + url = new URL(originalUrl, location); + } catch (final MalformedURLException e) { + throw new HttpDataSourceException(e, dataSpecToHandleRedirect, + PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, + HttpDataSourceException.TYPE_OPEN); + } + + // Check that the protocol of the new url is supported. + final String protocol = url.getProtocol(); + if (!"https".equals(protocol) && !"http".equals(protocol)) { + throw new HttpDataSourceException("Unsupported protocol redirect: " + protocol, + dataSpecToHandleRedirect, + PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, + HttpDataSourceException.TYPE_OPEN); + } + + if (!allowCrossProtocolRedirects && !protocol.equals(originalUrl.getProtocol())) { + throw new HttpDataSourceException( + "Disallowed cross-protocol redirect (" + + originalUrl.getProtocol() + + " to " + + protocol + + ")", + dataSpecToHandleRedirect, + PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, + HttpDataSourceException.TYPE_OPEN); + } + + return url; + } + + /** + * Attempts to skip the specified number of bytes in full. + * + * @param bytesToSkip The number of bytes to skip. + * @param dataSpecToUse The {@link DataSpec}. + * @throws IOException If the thread is interrupted during the operation, or if the data ended + * before skipping the specified number of bytes. + */ + @SuppressWarnings("checkstyle:FinalParameters") + private void skipFully(long bytesToSkip, final DataSpec dataSpecToUse) throws IOException { + if (bytesToSkip == 0) { + return; + } + + final byte[] skipBuffer = new byte[4096]; + while (bytesToSkip > 0) { + final int readLength = (int) min(bytesToSkip, skipBuffer.length); + final int read = castNonNull(inputStream).read(skipBuffer, 0, readLength); + if (Thread.currentThread().isInterrupted()) { + throw new HttpDataSourceException( + new InterruptedIOException(), + dataSpecToUse, + PlaybackException.ERROR_CODE_IO_UNSPECIFIED, + HttpDataSourceException.TYPE_OPEN); + } + + if (read == -1) { + throw new HttpDataSourceException( + dataSpecToUse, + PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE, + HttpDataSourceException.TYPE_OPEN); + } + + bytesToSkip -= read; + bytesTransferred(read); + } + } + + /** + * Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at + * index {@code offset}. + * + *

+ * This method blocks until at least one byte of data can be read, the end of the opened range + * is detected, or an exception is thrown. + *

+ * + * @param buffer The buffer into which the read data should be stored. + * @param offset The start offset into {@code buffer} at which data should be written. + * @param readLength The maximum number of bytes to read. + * @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened + * range is reached. + * @throws IOException If an error occurs reading from the source. + */ + @SuppressWarnings("checkstyle:FinalParameters") + private int readInternal(final byte[] buffer, final int offset, int readLength) + throws IOException { + if (readLength == 0) { + return 0; + } + if (bytesToRead != C.LENGTH_UNSET) { + final long bytesRemaining = bytesToRead - bytesRead; + if (bytesRemaining == 0) { + return C.RESULT_END_OF_INPUT; + } + readLength = (int) min(readLength, bytesRemaining); + } + + final int read = castNonNull(inputStream).read(buffer, offset, readLength); + if (read == -1) { + return C.RESULT_END_OF_INPUT; + } + + bytesRead += read; + bytesTransferred(read); + return read; + } + + /** + * On platform API levels 19 and 20, okhttp's implementation of {@link InputStream#close} can + * block for a long time if the stream has a lot of data remaining. Call this method before + * closing the input stream to make a best effort to cause the input stream to encounter an + * unexpected end of input, working around this issue. On other platform API levels, the method + * does nothing. + * + * @param connection The connection whose {@link InputStream} should be terminated. + * @param bytesRemaining The number of bytes remaining to be read from the input stream if its + * length is known. {@link C#LENGTH_UNSET} otherwise. + */ + private static void maybeTerminateInputStream(@Nullable final HttpURLConnection connection, + final long bytesRemaining) { + if (connection == null || Util.SDK_INT < 19 || Util.SDK_INT > 20) { + return; + } + + try { + final InputStream inputStream = connection.getInputStream(); + if (bytesRemaining == C.LENGTH_UNSET) { + // If the input stream has already ended, do nothing. The socket may be re-used. + if (inputStream.read() == -1) { + return; + } + } else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) { + // There isn't much data left. Prefer to allow it to drain, which may allow the + // socket to be re-used. + return; + } + final String className = inputStream.getClass().getName(); + if ("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream" + .equals(className) + || "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream" + .equals(className)) { + final Class superclass = inputStream.getClass().getSuperclass(); + final Method unexpectedEndOfInput = checkNotNull(superclass).getDeclaredMethod( + "unexpectedEndOfInput"); + unexpectedEndOfInput.setAccessible(true); + unexpectedEndOfInput.invoke(inputStream); + } + } catch (final Exception e) { + // If an IOException then the connection didn't ever have an input stream, or it was + // closed already. If another type of exception then something went wrong, most likely + // the device isn't using okhttp. + } + } + + /** + * Closes the current connection quietly, if there is one. + */ + private void closeConnectionQuietly() { + if (connection != null) { + try { + connection.disconnect(); + } catch (final Exception e) { + Log.e(TAG, "Unexpected error while disconnecting", e); + } + connection = null; + } + } + + private static boolean isCompressed(@NonNull final HttpURLConnection connection) { + final String contentEncoding = connection.getHeaderField("Content-Encoding"); + return "gzip".equalsIgnoreCase(contentEncoding); + } + + /** + * Builds a {@code range} parameter for the given position and length. + * + *

+ * To fetch its contents, YouTube use range requests which append a {@code range} parameter + * to videoplayback URLs instead of the {@code Range} header (even if the server respond + * correctly when requesting a range of a ressouce with it). + *

+ * + *

+ * The parameter works in the same way as the header. + *

+ * + * @param position The request position. + * @param length The request length, or {@link C#LENGTH_UNSET} if the request is unbounded. + * @return The corresponding {@code range} parameter, or {@code null} if this parameter is + * unnecessary because the whole resource is being requested. + */ + @Nullable + private static String buildRangeParameter(final long position, final long length) { + if (position == 0 && length == C.LENGTH_UNSET) { + return null; + } + + final StringBuilder rangeParameter = new StringBuilder(); + rangeParameter.append("&range="); + rangeParameter.append(position); + rangeParameter.append("-"); + if (length != C.LENGTH_UNSET) { + rangeParameter.append(position + length - 1); + } + return rangeParameter.toString(); + } + + private static final class NullFilteringHeadersMap + extends ForwardingMap> { + private final Map> headers; + + NullFilteringHeadersMap(final Map> headers) { + this.headers = headers; + } + + @NonNull + @Override + protected Map> delegate() { + return headers; + } + + @Override + public boolean containsKey(@Nullable final Object key) { + return key != null && super.containsKey(key); + } + + @Nullable + @Override + public List get(@Nullable final Object key) { + return key == null ? null : super.get(key); + } + + @NonNull + @Override + public Set keySet() { + return Sets.filter(super.keySet(), Objects::nonNull); + } + + @NonNull + @Override + public Set>> entrySet() { + return Sets.filter(super.entrySet(), entry -> entry.getKey() != null); + } + + @Override + public int size() { + return super.size() - (super.containsKey(null) ? 1 : 0); + } + + @Override + public boolean isEmpty() { + return super.isEmpty() || (super.size() == 1 && super.containsKey(null)); + } + + @Override + public boolean containsValue(@Nullable final Object value) { + return super.standardContainsValue(value); + } + + @Override + public boolean equals(@Nullable final Object object) { + return object != null && super.standardEquals(object); + } + + @Override + public int hashCode() { + return super.standardHashCode(); + } + } +} + diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java index 794fe9b3c..a7fb40c47 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java @@ -126,6 +126,14 @@ public class PlayerGestureListener } private void onScrollMainVolume(final float distanceX, final float distanceY) { + // If we just started sliding, change the progress bar to match the system volume + if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) { + final float volumePercent = player + .getAudioReactor().getVolume() / (float) maxVolume; + player.getVolumeProgressBar().setProgress( + (int) (volumePercent * player.getMaxGestureLength())); + } + player.getVolumeProgressBar().incrementProgressBy((int) distanceY); final float currentProgressPercent = (float) player .getVolumeProgressBar().getProgress() / player.getMaxGestureLength(); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java index 98e04d466..d189616d1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java @@ -1,96 +1,46 @@ package org.schabi.newpipe.player.helper; import android.content.Context; -import android.util.Log; -import com.google.android.exoplayer2.database.StandaloneDatabaseProvider; +import androidx.annotation.NonNull; + import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSource; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.cache.CacheDataSink; import com.google.android.exoplayer2.upstream.cache.CacheDataSource; -import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; -import java.io.File; +final class CacheFactory implements DataSource.Factory { + private static final int CACHE_FLAGS = CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR; -import androidx.annotation.NonNull; + private final Context context; + private final TransferListener transferListener; + private final DataSource.Factory upstreamDataSourceFactory; + private final SimpleCache cache; -/* package-private */ class CacheFactory implements DataSource.Factory { - private static final String TAG = "CacheFactory"; - - private static final String CACHE_FOLDER_NAME = "exoplayer"; - private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE - | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR; - - private final DataSource.Factory dataSourceFactory; - private final File cacheDir; - private final long maxFileSize; - - // Creating cache on every instance may cause problems with multiple players when - // sources are not ExtractorMediaSource - // see: https://stackoverflow.com/questions/28700391/using-cache-in-exoplayer - // todo: make this a singleton? - private static SimpleCache cache; - - CacheFactory(@NonNull final Context context, - @NonNull final String userAgent, - @NonNull final TransferListener transferListener) { - this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(), - PlayerHelper.getPreferredFileSize()); - } - - private CacheFactory(@NonNull final Context context, - @NonNull final String userAgent, - @NonNull final TransferListener transferListener, - final long maxCacheSize, - final long maxFileSize) { - this.maxFileSize = maxFileSize; - - dataSourceFactory = new DefaultDataSource - .Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent)) - .setTransferListener(transferListener); - cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME); - if (!cacheDir.exists()) { - //noinspection ResultOfMethodCallIgnored - cacheDir.mkdir(); - } - - if (cache == null) { - final LeastRecentlyUsedCacheEvictor evictor - = new LeastRecentlyUsedCacheEvictor(maxCacheSize); - cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context)); - } + CacheFactory(final Context context, + final TransferListener transferListener, + final SimpleCache cache, + final DataSource.Factory upstreamDataSourceFactory) { + this.context = context; + this.transferListener = transferListener; + this.cache = cache; + this.upstreamDataSourceFactory = upstreamDataSourceFactory; } @NonNull @Override public DataSource createDataSource() { - Log.d(TAG, "initExoPlayerCache: cacheDir = " + cacheDir.getAbsolutePath()); + final DefaultDataSource dataSource = new DefaultDataSource.Factory(context, + upstreamDataSourceFactory) + .setTransferListener(transferListener) + .createDataSource(); - final DataSource dataSource = dataSourceFactory.createDataSource(); final FileDataSource fileSource = new FileDataSource(); - final CacheDataSink dataSink = new CacheDataSink(cache, maxFileSize); - + final CacheDataSink dataSink + = new CacheDataSink(cache, PlayerHelper.getPreferredFileSize()); return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null); } - - public void tryDeleteCacheFiles() { - if (!cacheDir.exists() || !cacheDir.isDirectory()) { - return; - } - - try { - for (final File file : cacheDir.listFiles()) { - final String filePath = file.getAbsolutePath(); - final boolean deleteSuccessful = file.delete(); - - Log.d(TAG, "tryDeleteCacheFiles: " + filePath + " deleted = " + deleteSuccessful); - } - } catch (final Exception e) { - Log.e(TAG, "Failed to delete file.", e); - } - } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java index c12ba754a..a8735dc08 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java @@ -13,6 +13,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.media.session.MediaButtonReceiver; +import com.google.android.exoplayer2.ForwardingPlayer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; @@ -55,7 +56,17 @@ public class MediaSessionManager { sessionConnector = new MediaSessionConnector(mediaSession); sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback)); - sessionConnector.setPlayer(player); + sessionConnector.setPlayer(new ForwardingPlayer(player) { + @Override + public void play() { + callback.play(); + } + + @Override + public void pause() { + callback.pause(); + } + }); } @Nullable diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java index 1a55c21c3..19a5a645b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -1,123 +1,121 @@ package org.schabi.newpipe.player.helper; +import static org.schabi.newpipe.ktx.ViewUtils.animateRotation; import static org.schabi.newpipe.player.Player.DEBUG; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; +import static org.schabi.newpipe.util.ThemeHelper.resolveDrawable; import android.app.Dialog; import android.content.Context; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; import android.os.Bundle; import android.util.Log; +import android.view.LayoutInflater; import android.view.View; import android.widget.CheckBox; -import android.widget.RelativeLayout; import android.widget.SeekBar; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.DialogFragment; import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding; +import org.schabi.newpipe.player.Player; import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener; import org.schabi.newpipe.util.SliderStrategy; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.DoubleConsumer; +import java.util.function.DoubleFunction; +import java.util.function.DoubleSupplier; + +import icepick.Icepick; +import icepick.State; + public class PlaybackParameterDialog extends DialogFragment { + private static final String TAG = "PlaybackParameterDialog"; + // Minimum allowable range in ExoPlayer - private static final double MINIMUM_PLAYBACK_VALUE = 0.10f; - private static final double MAXIMUM_PLAYBACK_VALUE = 3.00f; + private static final double MIN_PITCH_OR_SPEED = 0.10f; + private static final double MAX_PITCH_OR_SPEED = 3.00f; - private static final char STEP_UP_SIGN = '+'; - private static final char STEP_DOWN_SIGN = '-'; + private static final boolean PITCH_CTRL_MODE_PERCENT = false; + private static final boolean PITCH_CTRL_MODE_SEMITONE = true; - private static final double STEP_ONE_PERCENT_VALUE = 0.01f; - private static final double STEP_FIVE_PERCENT_VALUE = 0.05f; - private static final double STEP_TEN_PERCENT_VALUE = 0.10f; - private static final double STEP_TWENTY_FIVE_PERCENT_VALUE = 0.25f; - private static final double STEP_ONE_HUNDRED_PERCENT_VALUE = 1.00f; + private static final double STEP_1_PERCENT_VALUE = 0.01f; + private static final double STEP_5_PERCENT_VALUE = 0.05f; + private static final double STEP_10_PERCENT_VALUE = 0.10f; + private static final double STEP_25_PERCENT_VALUE = 0.25f; + private static final double STEP_100_PERCENT_VALUE = 1.00f; private static final double DEFAULT_TEMPO = 1.00f; - private static final double DEFAULT_PITCH = 1.00f; - private static final int DEFAULT_SEMITONES = 0; - private static final double DEFAULT_STEP = STEP_TWENTY_FIVE_PERCENT_VALUE; + private static final double DEFAULT_PITCH_PERCENT = 1.00f; + private static final double DEFAULT_STEP = STEP_25_PERCENT_VALUE; private static final boolean DEFAULT_SKIP_SILENCE = false; - @NonNull - private static final String TAG = "PlaybackParameterDialog"; - @NonNull - private static final String INITIAL_TEMPO_KEY = "initial_tempo_key"; - @NonNull - private static final String INITIAL_PITCH_KEY = "initial_pitch_key"; + private static final SliderStrategy QUADRATIC_STRATEGY = new SliderStrategy.Quadratic( + MIN_PITCH_OR_SPEED, + MAX_PITCH_OR_SPEED, + 1.00f, + 10_000); - @NonNull - private static final String TEMPO_KEY = "tempo_key"; - @NonNull - private static final String PITCH_KEY = "pitch_key"; - @NonNull - private static final String STEP_SIZE_KEY = "step_size_key"; + private static final SliderStrategy SEMITONE_STRATEGY = new SliderStrategy() { + @Override + public int progressOf(final double value) { + return PlayerSemitoneHelper.percentToSemitones(value) + 12; + } - @NonNull - private final SliderStrategy strategy = new SliderStrategy.Quadratic( - MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE, - /*centerAt=*/1.00f, /*sliderGranularity=*/10000); + @Override + public double valueOf(final int progress) { + return PlayerSemitoneHelper.semitonesToPercent(progress - 12); + } + }; @Nullable private Callback callback; - private double initialTempo = DEFAULT_TEMPO; - private double initialPitch = DEFAULT_PITCH; - private int initialSemitones = DEFAULT_SEMITONES; - private boolean initialSkipSilence = DEFAULT_SKIP_SILENCE; - private double tempo = DEFAULT_TEMPO; - private double pitch = DEFAULT_PITCH; - private int semitones = DEFAULT_SEMITONES; + @State + double initialTempo = DEFAULT_TEMPO; + @State + double initialPitchPercent = DEFAULT_PITCH_PERCENT; + @State + boolean initialSkipSilence = DEFAULT_SKIP_SILENCE; - @Nullable - private SeekBar tempoSlider; - @Nullable - private TextView tempoCurrentText; - @Nullable - private TextView tempoStepDownText; - @Nullable - private TextView tempoStepUpText; - @Nullable - private SeekBar pitchSlider; - @Nullable - private TextView pitchCurrentText; - @Nullable - private TextView pitchStepDownText; - @Nullable - private TextView pitchStepUpText; - @Nullable - private SeekBar semitoneSlider; - @Nullable - private TextView semitoneCurrentText; - @Nullable - private TextView semitoneStepDownText; - @Nullable - private TextView semitoneStepUpText; - @Nullable - private CheckBox unhookingCheckbox; - @Nullable - private CheckBox skipSilenceCheckbox; - @Nullable - private CheckBox adjustBySemitonesCheckbox; + @State + double tempo = DEFAULT_TEMPO; + @State + double pitchPercent = DEFAULT_PITCH_PERCENT; + @State + boolean skipSilence = DEFAULT_SKIP_SILENCE; - public static PlaybackParameterDialog newInstance(final double playbackTempo, - final double playbackPitch, - final boolean playbackSkipSilence, - final Callback callback) { + private DialogPlaybackParameterBinding binding; + + public static PlaybackParameterDialog newInstance( + final double playbackTempo, + final double playbackPitch, + final boolean playbackSkipSilence, + final Callback callback + ) { final PlaybackParameterDialog dialog = new PlaybackParameterDialog(); dialog.callback = callback; + dialog.initialTempo = playbackTempo; - dialog.initialPitch = playbackPitch; - - dialog.tempo = playbackTempo; - dialog.pitch = playbackPitch; - dialog.semitones = dialog.percentToSemitones(playbackPitch); - + dialog.initialPitchPercent = playbackPitch; dialog.initialSkipSilence = playbackSkipSilence; + + dialog.tempo = dialog.initialTempo; + dialog.pitchPercent = dialog.initialPitchPercent; + dialog.skipSilence = dialog.initialSkipSilence; + return dialog; } @@ -135,29 +133,10 @@ public class PlaybackParameterDialog extends DialogFragment { } } - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - assureCorrectAppLanguage(getContext()); - super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO); - initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH); - initialSemitones = percentToSemitones(initialPitch); - - tempo = savedInstanceState.getDouble(TEMPO_KEY, DEFAULT_TEMPO); - pitch = savedInstanceState.getDouble(PITCH_KEY, DEFAULT_PITCH); - semitones = percentToSemitones(pitch); - } - } - @Override public void onSaveInstanceState(@NonNull final Bundle outState) { super.onSaveInstanceState(outState); - outState.putDouble(INITIAL_TEMPO_KEY, initialTempo); - outState.putDouble(INITIAL_PITCH_KEY, initialPitch); - - outState.putDouble(TEMPO_KEY, getCurrentTempo()); - outState.putDouble(PITCH_KEY, getCurrentPitch()); + Icepick.saveInstanceState(this, outState); } /*////////////////////////////////////////////////////////////////////////// @@ -168,327 +147,345 @@ public class PlaybackParameterDialog extends DialogFragment { @Override public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { assureCorrectAppLanguage(getContext()); - final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null); - setupControlViews(view); + Icepick.restoreInstanceState(this, savedInstanceState); + + binding = DialogPlaybackParameterBinding.inflate(LayoutInflater.from(getContext())); + initUI(); final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity()) - .setView(view) + .setView(binding.getRoot()) .setCancelable(true) - .setNegativeButton(R.string.cancel, (dialogInterface, i) -> - setPlaybackParameters(initialTempo, initialPitch, - initialSemitones, initialSkipSilence)) - .setNeutralButton(R.string.playback_reset, (dialogInterface, i) -> - setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, - DEFAULT_SEMITONES, DEFAULT_SKIP_SILENCE)) - .setPositiveButton(R.string.ok, (dialogInterface, i) -> - setCurrentPlaybackParameters()); + .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { + setAndUpdateTempo(initialTempo); + setAndUpdatePitch(initialPitchPercent); + setAndUpdateSkipSilence(initialSkipSilence); + updateCallback(); + }) + .setNeutralButton(R.string.playback_reset, (dialogInterface, i) -> { + setAndUpdateTempo(DEFAULT_TEMPO); + setAndUpdatePitch(DEFAULT_PITCH_PERCENT); + setAndUpdateSkipSilence(DEFAULT_SKIP_SILENCE); + updateCallback(); + }) + .setPositiveButton(R.string.ok, (dialogInterface, i) -> updateCallback()); return dialogBuilder.create(); } /*////////////////////////////////////////////////////////////////////////// - // Control Views + // UI Initialization and Control //////////////////////////////////////////////////////////////////////////*/ - private void setupControlViews(@NonNull final View rootView) { - setupHookingControl(rootView); - setupSkipSilenceControl(rootView); - setupAdjustBySemitonesControl(rootView); + private void initUI() { + // Tempo + setText(binding.tempoMinimumText, PlayerHelper::formatSpeed, MIN_PITCH_OR_SPEED); + setText(binding.tempoMaximumText, PlayerHelper::formatSpeed, MAX_PITCH_OR_SPEED); - setupTempoControl(rootView); - setupPitchControl(rootView); - setupSemitoneControl(rootView); + binding.tempoSeekbar.setMax(QUADRATIC_STRATEGY.progressOf(MAX_PITCH_OR_SPEED)); + setAndUpdateTempo(tempo); + binding.tempoSeekbar.setOnSeekBarChangeListener( + getTempoOrPitchSeekbarChangeListener( + QUADRATIC_STRATEGY, + this::onTempoSliderUpdated)); - togglePitchSliderType(rootView); + registerOnStepClickListener( + binding.tempoStepDown, + () -> tempo, + -1, + this::onTempoSliderUpdated); + registerOnStepClickListener( + binding.tempoStepUp, + () -> tempo, + 1, + this::onTempoSliderUpdated); - setupStepSizeSelector(rootView); + // Pitch + binding.pitchToogleControlModes.setOnClickListener(v -> { + final boolean isCurrentlyVisible = + binding.pitchControlModeTabs.getVisibility() == View.GONE; + binding.pitchControlModeTabs.setVisibility(isCurrentlyVisible + ? View.VISIBLE + : View.GONE); + animateRotation(binding.pitchToogleControlModes, + Player.DEFAULT_CONTROLS_DURATION, + isCurrentlyVisible ? 180 : 0); + }); + + getPitchControlModeComponentMappings() + .forEach(this::setupPitchControlModeTextView); + // Initialization is done at the end + + // Pitch - Percent + setText(binding.pitchPercentMinimumText, PlayerHelper::formatPitch, MIN_PITCH_OR_SPEED); + setText(binding.pitchPercentMaximumText, PlayerHelper::formatPitch, MAX_PITCH_OR_SPEED); + + binding.pitchPercentSeekbar.setMax(QUADRATIC_STRATEGY.progressOf(MAX_PITCH_OR_SPEED)); + setAndUpdatePitch(pitchPercent); + binding.pitchPercentSeekbar.setOnSeekBarChangeListener( + getTempoOrPitchSeekbarChangeListener( + QUADRATIC_STRATEGY, + this::onPitchPercentSliderUpdated)); + + registerOnStepClickListener( + binding.pitchPercentStepDown, + () -> pitchPercent, + -1, + this::onPitchPercentSliderUpdated); + registerOnStepClickListener( + binding.pitchPercentStepUp, + () -> pitchPercent, + 1, + this::onPitchPercentSliderUpdated); + + // Pitch - Semitone + binding.pitchSemitoneSeekbar.setOnSeekBarChangeListener( + getTempoOrPitchSeekbarChangeListener( + SEMITONE_STRATEGY, + this::onPitchPercentSliderUpdated)); + + registerOnSemitoneStepClickListener( + binding.pitchSemitoneStepDown, + -1, + this::onPitchPercentSliderUpdated); + registerOnSemitoneStepClickListener( + binding.pitchSemitoneStepUp, + 1, + this::onPitchPercentSliderUpdated); + + // Steps + getStepSizeComponentMappings() + .forEach(this::setupStepTextView); + // Initialize UI + setStepSizeToUI(getCurrentStepSize()); + + // Bottom controls + bindCheckboxWithBoolPref( + binding.unhookCheckbox, + R.string.playback_unhook_key, + true, + isChecked -> { + if (!isChecked) { + // when unchecked, slide back to the minimum of current tempo or pitch + ensureHookIsValidAndUpdateCallBack(); + } + }); + + setAndUpdateSkipSilence(skipSilence); + binding.skipSilenceCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { + skipSilence = isChecked; + updateCallback(); + }); + + // PitchControlMode has to be initialized at the end because it requires the unhookCheckbox + changePitchControlMode(isCurrentPitchControlModeSemitone()); } - private void togglePitchSliderType(@NonNull final View rootView) { - final RelativeLayout pitchControl = rootView.findViewById(R.id.pitchControl); - final RelativeLayout semitoneControl = rootView.findViewById(R.id.semitoneControl); + // -- General formatting -- - final View separatorStepSizeSelector = - rootView.findViewById(R.id.separatorStepSizeSelector); - final RelativeLayout.LayoutParams params = - (RelativeLayout.LayoutParams) separatorStepSizeSelector.getLayoutParams(); - if (pitchControl != null && semitoneControl != null && unhookingCheckbox != null) { - if (getCurrentAdjustBySemitones()) { - // replaces pitchControl slider with semitoneControl slider - pitchControl.setVisibility(View.GONE); - semitoneControl.setVisibility(View.VISIBLE); - params.addRule(RelativeLayout.BELOW, R.id.semitoneControl); - - // forces unhook for semitones - unhookingCheckbox.setChecked(true); - unhookingCheckbox.setEnabled(false); - - setupTempoStepSizeSelector(rootView); - } else { - semitoneControl.setVisibility(View.GONE); - pitchControl.setVisibility(View.VISIBLE); - params.addRule(RelativeLayout.BELOW, R.id.pitchControl); - - // (re)enables hooking selection - unhookingCheckbox.setEnabled(true); - setupCombinedStepSizeSelector(rootView); - } - } + private void setText( + final TextView textView, + final DoubleFunction formatter, + final double value + ) { + Objects.requireNonNull(textView).setText(formatter.apply(value)); } - private void setupTempoControl(@NonNull final View rootView) { - tempoSlider = rootView.findViewById(R.id.tempoSeekbar); - final TextView tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText); - final TextView tempoMaximumText = rootView.findViewById(R.id.tempoMaximumText); - tempoCurrentText = rootView.findViewById(R.id.tempoCurrentText); - tempoStepUpText = rootView.findViewById(R.id.tempoStepUp); - tempoStepDownText = rootView.findViewById(R.id.tempoStepDown); + // -- Steps -- - if (tempoCurrentText != null) { - tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo)); - } - if (tempoMaximumText != null) { - tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE)); - } - if (tempoMinimumText != null) { - tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE)); - } - - if (tempoSlider != null) { - tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); - tempoSlider.setProgress(strategy.progressOf(tempo)); - tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener()); - } + private void registerOnStepClickListener( + final TextView stepTextView, + final DoubleSupplier currentValueSupplier, + final double direction, // -1 for step down, +1 for step up + final DoubleConsumer newValueConsumer + ) { + stepTextView.setOnClickListener(view -> { + newValueConsumer.accept( + currentValueSupplier.getAsDouble() + 1 * getCurrentStepSize() * direction); + updateCallback(); + }); } - private void setupPitchControl(@NonNull final View rootView) { - pitchSlider = rootView.findViewById(R.id.pitchSeekbar); - final TextView pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText); - final TextView pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText); - pitchCurrentText = rootView.findViewById(R.id.pitchCurrentText); - pitchStepDownText = rootView.findViewById(R.id.pitchStepDown); - pitchStepUpText = rootView.findViewById(R.id.pitchStepUp); - - if (pitchCurrentText != null) { - pitchCurrentText.setText(PlayerHelper.formatPitch(pitch)); - } - if (pitchMaximumText != null) { - pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE)); - } - if (pitchMinimumText != null) { - pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE)); - } - - if (pitchSlider != null) { - pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE)); - pitchSlider.setProgress(strategy.progressOf(pitch)); - pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener()); - } + private void registerOnSemitoneStepClickListener( + final TextView stepTextView, + final int direction, // -1 for step down, +1 for step up + final DoubleConsumer newValueConsumer + ) { + stepTextView.setOnClickListener(view -> { + newValueConsumer.accept(PlayerSemitoneHelper.semitonesToPercent( + PlayerSemitoneHelper.percentToSemitones(this.pitchPercent) + direction)); + updateCallback(); + }); } - private void setupSemitoneControl(@NonNull final View rootView) { - semitoneSlider = rootView.findViewById(R.id.semitoneSeekbar); - semitoneCurrentText = rootView.findViewById(R.id.semitoneCurrentText); - semitoneStepDownText = rootView.findViewById(R.id.semitoneStepDown); - semitoneStepUpText = rootView.findViewById(R.id.semitoneStepUp); + // -- Pitch -- - if (semitoneCurrentText != null) { - semitoneCurrentText.setText(getSignedSemitonesString(semitones)); - } - - if (semitoneSlider != null) { - setSemitoneSlider(semitones); - semitoneSlider.setOnSeekBarChangeListener(getOnSemitoneChangedListener()); - } - - } - - private void setupHookingControl(@NonNull final View rootView) { - unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox); - if (unhookingCheckbox != null) { - // restores whether pitch and tempo are unhooked or not - unhookingCheckbox.setChecked(PreferenceManager - .getDefaultSharedPreferences(requireContext()) - .getBoolean(getString(R.string.playback_unhook_key), true)); - - unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { - // saves whether pitch and tempo are unhooked or not - PreferenceManager.getDefaultSharedPreferences(requireContext()) - .edit() - .putBoolean(getString(R.string.playback_unhook_key), isChecked) - .apply(); - - if (!isChecked) { - // when unchecked, slides back to the minimum of current tempo or pitch - final double minimum = Math.min(getCurrentPitch(), getCurrentTempo()); - setSliders(minimum); - setCurrentPlaybackParameters(); - } - }); - } - } - - private void setupSkipSilenceControl(@NonNull final View rootView) { - skipSilenceCheckbox = rootView.findViewById(R.id.skipSilenceCheckbox); - if (skipSilenceCheckbox != null) { - skipSilenceCheckbox.setChecked(initialSkipSilence); - skipSilenceCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> - setCurrentPlaybackParameters()); - } - } - - private void setupAdjustBySemitonesControl(@NonNull final View rootView) { - adjustBySemitonesCheckbox = rootView.findViewById(R.id.adjustBySemitonesCheckbox); - if (adjustBySemitonesCheckbox != null) { - // restores whether semitone adjustment is used or not - adjustBySemitonesCheckbox.setChecked(PreferenceManager - .getDefaultSharedPreferences(requireContext()) - .getBoolean(getString(R.string.playback_adjust_by_semitones_key), true)); - - // stores whether semitone adjustment is used or not - adjustBySemitonesCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { - PreferenceManager.getDefaultSharedPreferences(requireContext()) + private void setupPitchControlModeTextView( + final boolean semitones, + final TextView textView + ) { + textView.setOnClickListener(view -> { + PreferenceManager.getDefaultSharedPreferences(requireContext()) .edit() - .putBoolean(getString(R.string.playback_adjust_by_semitones_key), isChecked) + .putBoolean(getString(R.string.playback_adjust_by_semitones_key), semitones) .apply(); - togglePitchSliderType(rootView); - if (isChecked) { - setPlaybackParameters( - getCurrentTempo(), - getCurrentPitch(), - Integer.min(12, - Integer.max(-12, percentToSemitones(getCurrentPitch()) - )), - getCurrentSkipSilence() + + changePitchControlMode(semitones); + }); + } + + private Map getPitchControlModeComponentMappings() { + final Map mappings = new HashMap<>(); + mappings.put(PITCH_CTRL_MODE_PERCENT, binding.pitchControlModePercent); + mappings.put(PITCH_CTRL_MODE_SEMITONE, binding.pitchControlModeSemitone); + return mappings; + } + + private void changePitchControlMode(final boolean semitones) { + // Bring all textviews into a normal state + final Map pitchCtrlModeComponentMapping = + getPitchControlModeComponentMappings(); + pitchCtrlModeComponentMapping.forEach((v, textView) -> textView.setBackground( + resolveDrawable(requireContext(), R.attr.selectableItemBackground))); + + // Mark the selected textview + final TextView textView = pitchCtrlModeComponentMapping.get(semitones); + if (textView != null) { + textView.setBackground(new LayerDrawable(new Drawable[]{ + resolveDrawable(requireContext(), R.attr.dashed_border), + resolveDrawable(requireContext(), R.attr.selectableItemBackground) + })); + } + + // Show or hide component + binding.pitchPercentControl.setVisibility(semitones ? View.GONE : View.VISIBLE); + binding.pitchSemitoneControl.setVisibility(semitones ? View.VISIBLE : View.GONE); + + if (semitones) { + // Recalculate pitch percent when changing to semitone + // (as it could be an invalid semitone value) + final double newPitchPercent = calcValidPitch(pitchPercent); + + // If the values differ set the new pitch + if (this.pitchPercent != newPitchPercent) { + if (DEBUG) { + Log.d(TAG, "Bringing pitchPercent to correct corresponding semitone: " + + "currentPitchPercent = " + pitchPercent + ", " + + "newPitchPercent = " + newPitchPercent ); - setSemitoneSlider(Integer.min(12, - Integer.max(-12, percentToSemitones(getCurrentPitch())) - )); - } else { - setPlaybackParameters( - getCurrentTempo(), - semitonesToPercent(getCurrentSemitones()), - getCurrentSemitones(), - getCurrentSkipSilence() - ); - setPitchSlider(semitonesToPercent(getCurrentSemitones())); } - }); + this.onPitchPercentSliderUpdated(newPitchPercent); + updateCallback(); + } + } else if (!binding.unhookCheckbox.isChecked()) { + // When changing to percent it's possible that tempo is != pitch + ensureHookIsValidAndUpdateCallBack(); } } - private void setupStepSizeSelector(@NonNull final View rootView) { - setStepSize(PreferenceManager + private boolean isCurrentPitchControlModeSemitone() { + return PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getBoolean( + getString(R.string.playback_adjust_by_semitones_key), + PITCH_CTRL_MODE_PERCENT); + } + + // -- Steps (Set) -- + + private void setupStepTextView( + final double stepSizeValue, + final TextView textView + ) { + setText(textView, PlaybackParameterDialog::getPercentString, stepSizeValue); + textView.setOnClickListener(view -> { + PreferenceManager.getDefaultSharedPreferences(requireContext()) + .edit() + .putFloat(getString(R.string.adjustment_step_key), (float) stepSizeValue) + .apply(); + + setStepSizeToUI(stepSizeValue); + }); + } + + private Map getStepSizeComponentMappings() { + final Map mappings = new HashMap<>(); + mappings.put(STEP_1_PERCENT_VALUE, binding.stepSizeOnePercent); + mappings.put(STEP_5_PERCENT_VALUE, binding.stepSizeFivePercent); + mappings.put(STEP_10_PERCENT_VALUE, binding.stepSizeTenPercent); + mappings.put(STEP_25_PERCENT_VALUE, binding.stepSizeTwentyFivePercent); + mappings.put(STEP_100_PERCENT_VALUE, binding.stepSizeOneHundredPercent); + return mappings; + } + + private void setStepSizeToUI(final double newStepSize) { + // Bring all textviews into a normal state + final Map stepSiteComponentMapping = getStepSizeComponentMappings(); + stepSiteComponentMapping.forEach((v, textView) -> textView.setBackground( + resolveDrawable(requireContext(), R.attr.selectableItemBackground))); + + // Mark the selected textview + final TextView textView = stepSiteComponentMapping.get(newStepSize); + if (textView != null) { + textView.setBackground(new LayerDrawable(new Drawable[]{ + resolveDrawable(requireContext(), R.attr.dashed_border), + resolveDrawable(requireContext(), R.attr.selectableItemBackground) + })); + } + + // Bind to the corresponding control components + binding.tempoStepUp.setText(getStepUpPercentString(newStepSize)); + binding.tempoStepDown.setText(getStepDownPercentString(newStepSize)); + + binding.pitchPercentStepUp.setText(getStepUpPercentString(newStepSize)); + binding.pitchPercentStepDown.setText(getStepDownPercentString(newStepSize)); + } + + private double getCurrentStepSize() { + return PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getFloat(getString(R.string.adjustment_step_key), (float) DEFAULT_STEP); + } + + // -- Additional options -- + + private void setAndUpdateSkipSilence(final boolean newSkipSilence) { + this.skipSilence = newSkipSilence; + binding.skipSilenceCheckbox.setChecked(newSkipSilence); + } + + @SuppressWarnings("SameParameterValue") // this method was written to be reusable + private void bindCheckboxWithBoolPref( + @NonNull final CheckBox checkBox, + @StringRes final int resId, + final boolean defaultValue, + @NonNull final Consumer onInitialValueOrValueChange + ) { + final boolean prefValue = PreferenceManager .getDefaultSharedPreferences(requireContext()) - .getFloat(getString(R.string.adjustment_step_key), (float) DEFAULT_STEP)); + .getBoolean(getString(resId), defaultValue); - final TextView stepSizeOnePercentText = rootView.findViewById(R.id.stepSizeOnePercent); - final TextView stepSizeFivePercentText = rootView.findViewById(R.id.stepSizeFivePercent); - final TextView stepSizeTenPercentText = rootView.findViewById(R.id.stepSizeTenPercent); - final TextView stepSizeTwentyFivePercentText = rootView - .findViewById(R.id.stepSizeTwentyFivePercent); - final TextView stepSizeOneHundredPercentText = rootView - .findViewById(R.id.stepSizeOneHundredPercent); + checkBox.setChecked(prefValue); - if (stepSizeOnePercentText != null) { - stepSizeOnePercentText.setText(getPercentString(STEP_ONE_PERCENT_VALUE)); - stepSizeOnePercentText - .setOnClickListener(view -> setStepSize(STEP_ONE_PERCENT_VALUE)); - } + onInitialValueOrValueChange.accept(prefValue); - if (stepSizeFivePercentText != null) { - stepSizeFivePercentText.setText(getPercentString(STEP_FIVE_PERCENT_VALUE)); - stepSizeFivePercentText - .setOnClickListener(view -> setStepSize(STEP_FIVE_PERCENT_VALUE)); - } + checkBox.setOnCheckedChangeListener((compoundButton, isChecked) -> { + // save whether pitch and tempo are unhooked or not + PreferenceManager.getDefaultSharedPreferences(requireContext()) + .edit() + .putBoolean(getString(resId), isChecked) + .apply(); - if (stepSizeTenPercentText != null) { - stepSizeTenPercentText.setText(getPercentString(STEP_TEN_PERCENT_VALUE)); - stepSizeTenPercentText - .setOnClickListener(view -> setStepSize(STEP_TEN_PERCENT_VALUE)); - } - - if (stepSizeTwentyFivePercentText != null) { - stepSizeTwentyFivePercentText - .setText(getPercentString(STEP_TWENTY_FIVE_PERCENT_VALUE)); - stepSizeTwentyFivePercentText - .setOnClickListener(view -> setStepSize(STEP_TWENTY_FIVE_PERCENT_VALUE)); - } - - if (stepSizeOneHundredPercentText != null) { - stepSizeOneHundredPercentText - .setText(getPercentString(STEP_ONE_HUNDRED_PERCENT_VALUE)); - stepSizeOneHundredPercentText - .setOnClickListener(view -> setStepSize(STEP_ONE_HUNDRED_PERCENT_VALUE)); - } + onInitialValueOrValueChange.accept(isChecked); + }); } - private void setupTempoStepSizeSelector(@NonNull final View rootView) { - final TextView playbackStepTypeText = rootView.findViewById(R.id.playback_step_type); - if (playbackStepTypeText != null) { - playbackStepTypeText.setText(R.string.playback_tempo_step); - } - setupStepSizeSelector(rootView); - } - - private void setupCombinedStepSizeSelector(@NonNull final View rootView) { - final TextView playbackStepTypeText = rootView.findViewById(R.id.playback_step_type); - if (playbackStepTypeText != null) { - playbackStepTypeText.setText(R.string.playback_step); - } - setupStepSizeSelector(rootView); - } - - private void setStepSize(final double stepSize) { - PreferenceManager.getDefaultSharedPreferences(requireContext()) - .edit() - .putFloat(getString(R.string.adjustment_step_key), (float) stepSize) - .apply(); - - if (tempoStepUpText != null) { - tempoStepUpText.setText(getStepUpPercentString(stepSize)); - tempoStepUpText.setOnClickListener(view -> { - onTempoSliderUpdated(getCurrentTempo() + stepSize); - setCurrentPlaybackParameters(); - }); - } - - if (tempoStepDownText != null) { - tempoStepDownText.setText(getStepDownPercentString(stepSize)); - tempoStepDownText.setOnClickListener(view -> { - onTempoSliderUpdated(getCurrentTempo() - stepSize); - setCurrentPlaybackParameters(); - }); - } - - if (pitchStepUpText != null) { - pitchStepUpText.setText(getStepUpPercentString(stepSize)); - pitchStepUpText.setOnClickListener(view -> { - onPitchSliderUpdated(getCurrentPitch() + stepSize); - setCurrentPlaybackParameters(); - }); - } - - if (pitchStepDownText != null) { - pitchStepDownText.setText(getStepDownPercentString(stepSize)); - pitchStepDownText.setOnClickListener(view -> { - onPitchSliderUpdated(getCurrentPitch() - stepSize); - setCurrentPlaybackParameters(); - }); - } - - if (semitoneStepDownText != null) { - semitoneStepDownText.setOnClickListener(view -> { - onSemitoneSliderUpdated(getCurrentSemitones() - 1); - setCurrentPlaybackParameters(); - }); - } - - if (semitoneStepUpText != null) { - semitoneStepUpText.setOnClickListener(view -> { - onSemitoneSliderUpdated(getCurrentSemitones() + 1); - setCurrentPlaybackParameters(); - }); + /** + * Ensures that the slider hook is valid and if not sets and updates the sliders accordingly. + *
+ * You have to ensure by yourself that the hooking is active. + */ + private void ensureHookIsValidAndUpdateCallBack() { + if (tempo != pitchPercent) { + setSliders(Math.min(tempo, pitchPercent)); + updateCallback(); } } @@ -496,166 +493,106 @@ public class PlaybackParameterDialog extends DialogFragment { // Sliders //////////////////////////////////////////////////////////////////////////*/ - private SimpleOnSeekBarChangeListener getOnTempoChangedListener() { + private SeekBar.OnSeekBarChangeListener getTempoOrPitchSeekbarChangeListener( + final SliderStrategy sliderStrategy, + final DoubleConsumer newValueConsumer + ) { return new SimpleOnSeekBarChangeListener() { @Override - public void onProgressChanged(@NonNull final SeekBar seekBar, final int progress, + public void onProgressChanged(@NonNull final SeekBar seekBar, + final int progress, final boolean fromUser) { - final double currentTempo = strategy.valueOf(progress); - if (fromUser) { - onTempoSliderUpdated(currentTempo); - setCurrentPlaybackParameters(); - } - } - }; - } - - private SimpleOnSeekBarChangeListener getOnPitchChangedListener() { - return new SimpleOnSeekBarChangeListener() { - @Override - public void onProgressChanged(@NonNull final SeekBar seekBar, final int progress, - final boolean fromUser) { - final double currentPitch = strategy.valueOf(progress); - if (fromUser) { // this change is first in chain - onPitchSliderUpdated(currentPitch); - setCurrentPlaybackParameters(); - } - } - }; - } - - private SimpleOnSeekBarChangeListener getOnSemitoneChangedListener() { - return new SimpleOnSeekBarChangeListener() { - @Override - public void onProgressChanged(@NonNull final SeekBar seekBar, final int progress, - final boolean fromUser) { - // semitone slider supplies values 0 to 24, subtraction by 12 is required - final int currentSemitones = progress - 12; - if (fromUser) { // this change is first in chain - onSemitoneSliderUpdated(currentSemitones); - // line below also saves semitones as pitch percentages - onPitchSliderUpdated(semitonesToPercent(currentSemitones)); - setCurrentPlaybackParameters(); + if (fromUser) { // ensure that the user triggered the change + newValueConsumer.accept(sliderStrategy.valueOf(progress)); + updateCallback(); } } }; } private void onTempoSliderUpdated(final double newTempo) { - if (!unhookingCheckbox.isChecked()) { + if (!binding.unhookCheckbox.isChecked()) { setSliders(newTempo); } else { - setTempoSlider(newTempo); + setAndUpdateTempo(newTempo); } } - private void onPitchSliderUpdated(final double newPitch) { - if (!unhookingCheckbox.isChecked()) { + private void onPitchPercentSliderUpdated(final double newPitch) { + if (!binding.unhookCheckbox.isChecked()) { setSliders(newPitch); } else { - setPitchSlider(newPitch); + setAndUpdatePitch(newPitch); } } - private void onSemitoneSliderUpdated(final int newSemitone) { - setSemitoneSlider(newSemitone); - } - private void setSliders(final double newValue) { - setTempoSlider(newValue); - setPitchSlider(newValue); + setAndUpdateTempo(newValue); + setAndUpdatePitch(newValue); } - private void setTempoSlider(final double newTempo) { - if (tempoSlider == null) { - return; - } - tempoSlider.setProgress(strategy.progressOf(newTempo)); + private void setAndUpdateTempo(final double newTempo) { + this.tempo = calcValidTempo(newTempo); + + binding.tempoSeekbar.setProgress(QUADRATIC_STRATEGY.progressOf(tempo)); + setText(binding.tempoCurrentText, PlayerHelper::formatSpeed, tempo); } - private void setPitchSlider(final double newPitch) { - if (pitchSlider == null) { - return; - } - pitchSlider.setProgress(strategy.progressOf(newPitch)); + private void setAndUpdatePitch(final double newPitch) { + this.pitchPercent = calcValidPitch(newPitch); + + binding.pitchPercentSeekbar.setProgress(QUADRATIC_STRATEGY.progressOf(pitchPercent)); + binding.pitchSemitoneSeekbar.setProgress(SEMITONE_STRATEGY.progressOf(pitchPercent)); + setText(binding.pitchPercentCurrentText, + PlayerHelper::formatPitch, + pitchPercent); + setText(binding.pitchSemitoneCurrentText, + PlayerSemitoneHelper::formatPitchSemitones, + pitchPercent); } - private void setSemitoneSlider(final int newSemitone) { - if (semitoneSlider == null) { - return; + private double calcValidTempo(final double newTempo) { + return Math.max(MIN_PITCH_OR_SPEED, Math.min(MAX_PITCH_OR_SPEED, newTempo)); + } + + private double calcValidPitch(final double newPitch) { + final double calcPitch = + Math.max(MIN_PITCH_OR_SPEED, Math.min(MAX_PITCH_OR_SPEED, newPitch)); + + if (!isCurrentPitchControlModeSemitone()) { + return calcPitch; } - semitoneSlider.setProgress(newSemitone + 12); + + return PlayerSemitoneHelper.semitonesToPercent( + PlayerSemitoneHelper.percentToSemitones(calcPitch)); } /*////////////////////////////////////////////////////////////////////////// // Helper //////////////////////////////////////////////////////////////////////////*/ - private void setCurrentPlaybackParameters() { - if (getCurrentAdjustBySemitones()) { - setPlaybackParameters( - getCurrentTempo(), - semitonesToPercent(getCurrentSemitones()), - getCurrentSemitones(), - getCurrentSkipSilence() - ); - } else { - setPlaybackParameters( - getCurrentTempo(), - getCurrentPitch(), - percentToSemitones(getCurrentPitch()), - getCurrentSkipSilence() + private void updateCallback() { + if (callback == null) { + return; + } + if (DEBUG) { + Log.d(TAG, "Updating callback: " + + "tempo = " + tempo + ", " + + "pitchPercent = " + pitchPercent + ", " + + "skipSilence = " + skipSilence ); } - } - - private void setPlaybackParameters(final double newTempo, final double newPitch, - final int newSemitones, final boolean skipSilence) { - if (callback != null && tempoCurrentText != null - && pitchCurrentText != null && semitoneCurrentText != null) { - if (DEBUG) { - Log.d(TAG, "Setting playback parameters to " - + "tempo=[" + newTempo + "], " - + "pitch=[" + newPitch + "], " - + "semitones=[" + newSemitones + "]"); - } - - tempoCurrentText.setText(PlayerHelper.formatSpeed(newTempo)); - pitchCurrentText.setText(PlayerHelper.formatPitch(newPitch)); - semitoneCurrentText.setText(getSignedSemitonesString(newSemitones)); - callback.onPlaybackParameterChanged((float) newTempo, (float) newPitch, skipSilence); - } - } - - private double getCurrentTempo() { - return tempoSlider == null ? tempo : strategy.valueOf(tempoSlider.getProgress()); - } - - private double getCurrentPitch() { - return pitchSlider == null ? pitch : strategy.valueOf(pitchSlider.getProgress()); - } - - private int getCurrentSemitones() { - // semitoneSlider is absolute, that's why - 12 - return semitoneSlider == null ? semitones : semitoneSlider.getProgress() - 12; - } - - private boolean getCurrentSkipSilence() { - return skipSilenceCheckbox != null && skipSilenceCheckbox.isChecked(); - } - - private boolean getCurrentAdjustBySemitones() { - return adjustBySemitonesCheckbox != null && adjustBySemitonesCheckbox.isChecked(); + callback.onPlaybackParameterChanged((float) tempo, (float) pitchPercent, skipSilence); } @NonNull private static String getStepUpPercentString(final double percent) { - return STEP_UP_SIGN + getPercentString(percent); + return '+' + getPercentString(percent); } @NonNull private static String getStepDownPercentString(final double percent) { - return STEP_DOWN_SIGN + getPercentString(percent); + return '-' + getPercentString(percent); } @NonNull @@ -663,21 +600,8 @@ public class PlaybackParameterDialog extends DialogFragment { return PlayerHelper.formatPitch(percent); } - @NonNull - private static String getSignedSemitonesString(final int semitones) { - return semitones > 0 ? "+" + semitones : "" + semitones; - } - public interface Callback { void onPlaybackParameterChanged(float playbackTempo, float playbackPitch, boolean playbackSkipSilence); } - - public double semitonesToPercent(final int inSemitones) { - return Math.pow(2, inSemitones / 12.0); - } - - public int percentToSemitones(final double inPercent) { - return (int) Math.round(12 * Math.log(inPercent) / Math.log(2)); - } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java index 405f6fd37..88f25e194 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java @@ -1,7 +1,13 @@ package org.schabi.newpipe.player.helper; -import android.content.Context; +import static org.schabi.newpipe.MainActivity.DEBUG; +import android.content.Context; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.google.android.exoplayer2.database.StandaloneDatabaseProvider; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; @@ -13,12 +19,21 @@ import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; -import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; -import androidx.annotation.NonNull; +import org.schabi.newpipe.DownloaderImpl; +import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator; +import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubePostLiveStreamDvrDashManifestCreator; +import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator; +import org.schabi.newpipe.player.datasource.NonUriHlsDataSourceFactory; +import org.schabi.newpipe.player.datasource.YoutubeHttpDataSource; + +import java.io.File; public class PlayerDataSource { + public static final String TAG = PlayerDataSource.class.getSimpleName(); public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000; @@ -29,79 +44,174 @@ public class PlayerDataSource { * early. */ private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 15; - private static final int MANIFEST_MINIMUM_RETRY = 5; - private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE; - private final int continueLoadingCheckIntervalBytes; - private final DataSource.Factory cacheDataSourceFactory; + /** + * The maximum number of generated manifests per cache, in + * {@link YoutubeProgressiveDashManifestCreator}, {@link YoutubeOtfDashManifestCreator} and + * {@link YoutubePostLiveStreamDvrDashManifestCreator}. + */ + private static final int MAX_MANIFEST_CACHE_SIZE = 500; + + /** + * The folder name in which the ExoPlayer cache will be written. + */ + private static final String CACHE_FOLDER_NAME = "exoplayer"; + + /** + * The {@link SimpleCache} instance which will be used to build + * {@link com.google.android.exoplayer2.upstream.cache.CacheDataSource}s instances (with + * {@link CacheFactory}). + */ + private static SimpleCache cache; + + + private final int progressiveLoadIntervalBytes; + + // Generic Data Source Factories (without or with cache) private final DataSource.Factory cachelessDataSourceFactory; + private final CacheFactory cacheDataSourceFactory; - public PlayerDataSource(@NonNull final Context context, - @NonNull final String userAgent, - @NonNull final TransferListener transferListener) { - continueLoadingCheckIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context); - cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener); - cachelessDataSourceFactory = new DefaultDataSource - .Factory(context, new DefaultHttpDataSource.Factory().setUserAgent(userAgent)) + // YouTube-specific Data Source Factories (with cache) + // They use YoutubeHttpDataSource.Factory, with different parameters each + private final CacheFactory ytHlsCacheDataSourceFactory; + private final CacheFactory ytDashCacheDataSourceFactory; + private final CacheFactory ytProgressiveDashCacheDataSourceFactory; + + + public PlayerDataSource(final Context context, + final TransferListener transferListener) { + + progressiveLoadIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context); + + // make sure the static cache was created: needed by CacheFactories below + instantiateCacheIfNeeded(context); + + // generic data source factories use DefaultHttpDataSource.Factory + cachelessDataSourceFactory = new DefaultDataSource.Factory(context, + new DefaultHttpDataSource.Factory().setUserAgent(DownloaderImpl.USER_AGENT)) .setTransferListener(transferListener); + cacheDataSourceFactory = new CacheFactory(context, transferListener, cache, + new DefaultHttpDataSource.Factory().setUserAgent(DownloaderImpl.USER_AGENT)); + + // YouTube-specific data source factories use getYoutubeHttpDataSourceFactory() + ytHlsCacheDataSourceFactory = new CacheFactory(context, transferListener, cache, + getYoutubeHttpDataSourceFactory(false, false)); + ytDashCacheDataSourceFactory = new CacheFactory(context, transferListener, cache, + getYoutubeHttpDataSourceFactory(true, true)); + ytProgressiveDashCacheDataSourceFactory = new CacheFactory(context, transferListener, cache, + getYoutubeHttpDataSourceFactory(false, true)); + + // set the maximum size to manifest creators + YoutubeProgressiveDashManifestCreator.getCache().setMaximumSize(MAX_MANIFEST_CACHE_SIZE); + YoutubeOtfDashManifestCreator.getCache().setMaximumSize(MAX_MANIFEST_CACHE_SIZE); + YoutubePostLiveStreamDvrDashManifestCreator.getCache().setMaximumSize( + MAX_MANIFEST_CACHE_SIZE); } + + //region Live media source factories public SsMediaSource.Factory getLiveSsMediaSourceFactory() { - return new SsMediaSource.Factory( - new DefaultSsChunkSource.Factory(cachelessDataSourceFactory), - cachelessDataSourceFactory - ) - .setLoadErrorHandlingPolicy( - new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)) - .setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS); + return getSSMediaSourceFactory().setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS); } public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() { return new HlsMediaSource.Factory(cachelessDataSourceFactory) .setAllowChunklessPreparation(true) - .setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy( - MANIFEST_MINIMUM_RETRY)) .setPlaylistTrackerFactory((dataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory) -> new DefaultHlsPlaylistTracker(dataSourceFactory, loadErrorHandlingPolicy, - playlistParserFactory, PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) - ); + playlistParserFactory, + PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT)); } public DashMediaSource.Factory getLiveDashMediaSourceFactory() { return new DashMediaSource.Factory( getDefaultDashChunkSourceFactory(cachelessDataSourceFactory), - cachelessDataSourceFactory - ) - .setLoadErrorHandlingPolicy( - new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)); + cachelessDataSourceFactory); } + //endregion - private DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory( - final DataSource.Factory dataSourceFactory - ) { - return new DefaultDashChunkSource.Factory(dataSourceFactory); - } - public HlsMediaSource.Factory getHlsMediaSourceFactory() { + //region Generic media source factories + public HlsMediaSource.Factory getHlsMediaSourceFactory( + @Nullable final NonUriHlsDataSourceFactory.Builder hlsDataSourceFactoryBuilder) { + if (hlsDataSourceFactoryBuilder != null) { + hlsDataSourceFactoryBuilder.setDataSourceFactory(cacheDataSourceFactory); + return new HlsMediaSource.Factory(hlsDataSourceFactoryBuilder.build()); + } + return new HlsMediaSource.Factory(cacheDataSourceFactory); } public DashMediaSource.Factory getDashMediaSourceFactory() { return new DashMediaSource.Factory( getDefaultDashChunkSourceFactory(cacheDataSourceFactory), - cacheDataSourceFactory - ); + cacheDataSourceFactory); } - public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() { + public ProgressiveMediaSource.Factory getProgressiveMediaSourceFactory() { return new ProgressiveMediaSource.Factory(cacheDataSourceFactory) - .setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes) - .setLoadErrorHandlingPolicy( - new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY)); + .setContinueLoadingCheckIntervalBytes(progressiveLoadIntervalBytes); } - public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() { + public SsMediaSource.Factory getSSMediaSourceFactory() { + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(cachelessDataSourceFactory), + cachelessDataSourceFactory); + } + + public SingleSampleMediaSource.Factory getSingleSampleMediaSourceFactory() { return new SingleSampleMediaSource.Factory(cacheDataSourceFactory); } + //endregion + + + //region YouTube media source factories + public HlsMediaSource.Factory getYoutubeHlsMediaSourceFactory() { + return new HlsMediaSource.Factory(ytHlsCacheDataSourceFactory); + } + + public DashMediaSource.Factory getYoutubeDashMediaSourceFactory() { + return new DashMediaSource.Factory( + getDefaultDashChunkSourceFactory(ytDashCacheDataSourceFactory), + ytDashCacheDataSourceFactory); + } + + public ProgressiveMediaSource.Factory getYoutubeProgressiveMediaSourceFactory() { + return new ProgressiveMediaSource.Factory(ytProgressiveDashCacheDataSourceFactory) + .setContinueLoadingCheckIntervalBytes(progressiveLoadIntervalBytes); + } + //endregion + + + //region Static methods + private static DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory( + final DataSource.Factory dataSourceFactory) { + return new DefaultDashChunkSource.Factory(dataSourceFactory); + } + + private static YoutubeHttpDataSource.Factory getYoutubeHttpDataSourceFactory( + final boolean rangeParameterEnabled, + final boolean rnParameterEnabled) { + return new YoutubeHttpDataSource.Factory() + .setRangeParameterEnabled(rangeParameterEnabled) + .setRnParameterEnabled(rnParameterEnabled); + } + + private static void instantiateCacheIfNeeded(final Context context) { + if (cache == null) { + final File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME); + if (DEBUG) { + Log.d(TAG, "instantiateCacheIfNeeded: cacheDir = " + cacheDir.getAbsolutePath()); + } + if (!cacheDir.exists() && !cacheDir.mkdir()) { + Log.w(TAG, "instantiateCacheIfNeeded: could not create cache dir"); + } + + final LeastRecentlyUsedCacheEvictor evictor + = new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize()); + cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context)); + } + } + //endregion } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index b73c6cf7f..2131861bf 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -45,11 +45,9 @@ import com.google.android.exoplayer2.util.MimeTypes; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.SubtitlesStream; -import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.extractor.utils.Utils; import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.Player; @@ -110,12 +108,14 @@ public final class PlayerHelper { int MINIMIZE_ON_EXIT_MODE_POPUP = 2; } - private PlayerHelper() { } + private PlayerHelper() { + } //////////////////////////////////////////////////////////////////////////// // Exposed helpers //////////////////////////////////////////////////////////////////////////// + @NonNull public static String getTimeString(final int milliSeconds) { final int seconds = (milliSeconds % 60000) / 1000; final int minutes = (milliSeconds % 3600000) / 60000; @@ -131,15 +131,18 @@ public final class PlayerHelper { ).toString(); } + @NonNull public static String formatSpeed(final double speed) { return SPEED_FORMATTER.format(speed); } + @NonNull public static String formatPitch(final double pitch) { return PITCH_FORMATTER.format(pitch); } - public static String subtitleMimeTypesOf(final MediaFormat format) { + @NonNull + public static String subtitleMimeTypesOf(@NonNull final MediaFormat format) { switch (format) { case VTT: return MimeTypes.TEXT_VTT; @@ -190,18 +193,6 @@ public final class PlayerHelper { } } - @NonNull - public static String cacheKeyOf(@NonNull final StreamInfo info, - @NonNull final VideoStream video) { - return info.getUrl() + video.getResolution() + video.getFormat().getName(); - } - - @NonNull - public static String cacheKeyOf(@NonNull final StreamInfo info, - @NonNull final AudioStream audio) { - return info.getUrl() + audio.getAverageBitrate() + audio.getFormat().getName(); - } - /** * Given a {@link StreamInfo} and the existing queue items, * provide the {@link SinglePlayQueue} consisting of the next video for auto queueing. @@ -233,7 +224,7 @@ public final class PlayerHelper { return null; } - if (relatedItems.get(0) != null && relatedItems.get(0) instanceof StreamInfoItem + if (relatedItems.get(0) instanceof StreamInfoItem && !urls.contains(relatedItems.get(0).getUrl())) { return getAutoQueuedSinglePlayQueue((StreamInfoItem) relatedItems.get(0)); } @@ -335,6 +326,7 @@ public final class PlayerHelper { return 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE } + @NonNull public static ExoTrackSelection.Factory getQualitySelector() { return new AdaptiveTrackSelection.Factory( 1000, @@ -389,7 +381,7 @@ public final class PlayerHelper { /** * @param context the Android context * @return the screen brightness to use. A value less than 0 (the default) means to use the - * preferred screen brightness + * preferred screen brightness */ public static float getScreenBrightness(@NonNull final Context context) { final SharedPreferences sp = getPreferences(context); @@ -480,7 +472,8 @@ public final class PlayerHelper { return REPEAT_MODE_ONE; case REPEAT_MODE_ONE: return REPEAT_MODE_ALL; - case REPEAT_MODE_ALL: default: + case REPEAT_MODE_ALL: + default: return REPEAT_MODE_OFF; } } @@ -548,7 +541,7 @@ public final class PlayerHelper { player.getContext().getResources().getDimension(R.dimen.popup_default_width); final float popupWidth = popupRememberSizeAndPos ? player.getPrefs().getFloat(player.getContext().getString( - R.string.popup_saved_width_key), defaultSize) + R.string.popup_saved_width_key), defaultSize) : defaultSize; final float popupHeight = getMinimumVideoHeight(popupWidth); @@ -564,10 +557,10 @@ public final class PlayerHelper { final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f); popupLayoutParams.x = popupRememberSizeAndPos ? player.getPrefs().getInt(player.getContext().getString( - R.string.popup_saved_x_key), centerX) : centerX; + R.string.popup_saved_x_key), centerX) : centerX; popupLayoutParams.y = popupRememberSizeAndPos ? player.getPrefs().getInt(player.getContext().getString( - R.string.popup_saved_y_key), centerY) : centerY; + R.string.popup_saved_y_key), centerY) : centerY; return popupLayoutParams; } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java new file mode 100644 index 000000000..f3a71d7cd --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java @@ -0,0 +1,38 @@ +package org.schabi.newpipe.player.helper; + +/** + * Converts between percent and 12-tone equal temperament semitones. + *
+ * @see + * + * Wikipedia: Equal temperament#Twelve-tone equal temperament + * + */ +public final class PlayerSemitoneHelper { + public static final int SEMITONE_COUNT = 12; + + private PlayerSemitoneHelper() { + // No impl + } + + public static String formatPitchSemitones(final double percent) { + return formatPitchSemitones(percentToSemitones(percent)); + } + + public static String formatPitchSemitones(final int semitones) { + return semitones > 0 ? "+" + semitones : "" + semitones; + } + + public static double semitonesToPercent(final int semitones) { + return Math.pow(2, ensureSemitonesInRange(semitones) / (double) SEMITONE_COUNT); + } + + public static int percentToSemitones(final double percent) { + return ensureSemitonesInRange( + (int) Math.round(SEMITONE_COUNT * Math.log(percent) / Math.log(2))); + } + + private static int ensureSemitonesInRange(final int semitones) { + return Math.max(-SEMITONE_COUNT, Math.min(SEMITONE_COUNT, semitones)); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt b/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt index b103ac0e6..43e8288e6 100644 --- a/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt +++ b/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt @@ -32,7 +32,7 @@ class QualityClickListener( val videoStream = player.selectedVideoStream if (videoStream != null) { player.binding.qualityTextView.text = - MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.resolution + MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.getResolution() } player.saveWasPlaying() diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java index f2e98d866..e7aeb9638 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java @@ -5,9 +5,9 @@ import android.text.TextUtils; import android.view.MotionEvent; import android.view.View; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.PicassoHelper; +import org.schabi.newpipe.util.ServiceHelper; public class PlayQueueItemBuilder { private static final String TAG = PlayQueueItemBuilder.class.toString(); @@ -25,7 +25,7 @@ public class PlayQueueItemBuilder { holder.itemVideoTitleView.setText(item.getTitle()); } holder.itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.getUploader(), - NewPipe.getNameOfService(item.getServiceId()))); + ServiceHelper.getNameOfServiceById(item.getServiceId()))); if (item.getDuration() > 0) { holder.itemDurationView.setText(Localization.getDurationString(item.getDuration())); diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java index 9bded9331..934beba19 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/AudioPlaybackResolver.java @@ -1,22 +1,27 @@ package org.schabi.newpipe.player.resolver; +import static org.schabi.newpipe.util.ListHelper.getNonTorrentStreams; + import android.content.Context; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.MediaSource; -import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.helper.PlayerDataSource; -import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.mediaitem.MediaItemTag; import org.schabi.newpipe.player.mediaitem.StreamInfoTag; import org.schabi.newpipe.util.ListHelper; +import java.util.List; + public class AudioPlaybackResolver implements PlaybackResolver { + private static final String TAG = AudioPlaybackResolver.class.getSimpleName(); + @NonNull private final Context context; @NonNull @@ -31,19 +36,27 @@ public class AudioPlaybackResolver implements PlaybackResolver { @Override @Nullable public MediaSource resolve(@NonNull final StreamInfo info) { - final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info); + final MediaSource liveSource = PlaybackResolver.maybeBuildLiveMediaSource(dataSource, info); if (liveSource != null) { return liveSource; } - final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams()); + final List audioStreams = getNonTorrentStreams(info.getAudioStreams()); + + final int index = ListHelper.getDefaultAudioFormat(context, audioStreams); if (index < 0 || index >= info.getAudioStreams().size()) { return null; } final AudioStream audio = info.getAudioStreams().get(index); final MediaItemTag tag = StreamInfoTag.of(info); - return buildMediaSource(dataSource, audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio), - MediaFormat.getSuffixById(audio.getFormatId()), tag); + + try { + return PlaybackResolver.buildMediaSource( + dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag); + } catch (final ResolverException e) { + Log.e(TAG, "Unable to create audio source", e); + return null; + } } } diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java index 90b38ed51..34e7e9bd1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java @@ -1,50 +1,193 @@ package org.schabi.newpipe.player.resolver; +import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE; +import static org.schabi.newpipe.extractor.stream.VideoStream.RESOLUTION_UNKNOWN; +import static org.schabi.newpipe.player.helper.PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS; + import android.net.Uri; -import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.extractor.services.youtube.ItagItem; +import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.CreationException; +import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeOtfDashManifestCreator; +import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubePostLiveStreamDvrDashManifestCreator; +import org.schabi.newpipe.extractor.services.youtube.dashmanifestcreators.YoutubeProgressiveDashManifestCreator; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.DeliveryMethod; +import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamType; +import org.schabi.newpipe.extractor.stream.VideoStream; +import org.schabi.newpipe.player.datasource.NonUriHlsDataSourceFactory; import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.mediaitem.MediaItemTag; import org.schabi.newpipe.player.mediaitem.StreamInfoTag; import org.schabi.newpipe.util.StreamTypeUtil; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import static org.schabi.newpipe.player.helper.PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +/** + * This interface is just a shorthand for {@link Resolver} with {@link StreamInfo} as source and + * {@link MediaSource} as product. It contains many static methods that can be used by classes + * implementing this interface, and nothing else. + */ public interface PlaybackResolver extends Resolver { + String TAG = PlaybackResolver.class.getSimpleName(); + + //region Cache key generation + private static StringBuilder commonCacheKeyOf(final StreamInfo info, + final Stream stream, + final boolean resolutionOrBitrateUnknown) { + // stream info service id + final StringBuilder cacheKey = new StringBuilder(info.getServiceId()); + + // stream info id + cacheKey.append(" "); + cacheKey.append(info.getId()); + + // stream id (even if unknown) + cacheKey.append(" "); + cacheKey.append(stream.getId()); + + // mediaFormat (if not null) + final MediaFormat mediaFormat = stream.getFormat(); + if (mediaFormat != null) { + cacheKey.append(" "); + cacheKey.append(mediaFormat.getName()); + } + + // content (only if other information is missing) + // If the media format and the resolution/bitrate are both missing, then we don't have + // enough information to distinguish this stream from other streams. + // So, only in that case, we use the content (i.e. url or manifest) to differentiate + // between streams. + // Note that if the content were used even when other information is present, then two + // streams with the same stats but with different contents (e.g. because the url was + // refreshed) will be considered different (i.e. with a different cacheKey), making the + // cache useless. + if (resolutionOrBitrateUnknown && mediaFormat == null) { + cacheKey.append(" "); + cacheKey.append(Objects.hash(stream.getContent(), stream.getManifestUrl())); + } + + return cacheKey; + } + + /** + * Builds the cache key of a {@link VideoStream video stream}. + * + *

+ * A cache key is unique to the features of the provided video stream, and when possible + * independent of transient parameters (such as the URL of the stream). + * This ensures that there are no conflicts, but also that the cache is used as much as + * possible: the same cache should be used for two streams which have the same features but + * e.g. a different URL, since the URL might have been reloaded in the meantime, but the stream + * actually referenced by the URL is still the same. + *

+ * + * @param info the {@link StreamInfo stream info}, to distinguish between streams with + * the same features but coming from different stream infos + * @param videoStream the {@link VideoStream video stream} for which the cache key should be + * created + * @return a key to be used to store the cache of the provided {@link VideoStream video stream} + */ + static String cacheKeyOf(final StreamInfo info, final VideoStream videoStream) { + final boolean resolutionUnknown = videoStream.getResolution().equals(RESOLUTION_UNKNOWN); + final StringBuilder cacheKey = commonCacheKeyOf(info, videoStream, resolutionUnknown); + + // resolution (if known) + if (!resolutionUnknown) { + cacheKey.append(" "); + cacheKey.append(videoStream.getResolution()); + } + + // isVideoOnly + cacheKey.append(" "); + cacheKey.append(videoStream.isVideoOnly()); + + return cacheKey.toString(); + } + + /** + * Builds the cache key of an audio stream. + * + *

+ * A cache key is unique to the features of the provided {@link AudioStream audio stream}, and + * when possible independent of transient parameters (such as the URL of the stream). + * This ensures that there are no conflicts, but also that the cache is used as much as + * possible: the same cache should be used for two streams which have the same features but + * e.g. a different URL, since the URL might have been reloaded in the meantime, but the stream + * actually referenced by the URL is still the same. + *

+ * + * @param info the {@link StreamInfo stream info}, to distinguish between streams with + * the same features but coming from different stream infos + * @param audioStream the {@link AudioStream audio stream} for which the cache key should be + * created + * @return a key to be used to store the cache of the provided {@link AudioStream audio stream} + */ + static String cacheKeyOf(final StreamInfo info, final AudioStream audioStream) { + final boolean averageBitrateUnknown = audioStream.getAverageBitrate() == UNKNOWN_BITRATE; + final StringBuilder cacheKey = commonCacheKeyOf(info, audioStream, averageBitrateUnknown); + + // averageBitrate (if known) + if (!averageBitrateUnknown) { + cacheKey.append(" "); + cacheKey.append(audioStream.getAverageBitrate()); + } + + return cacheKey.toString(); + } + //endregion + + + //region Live media sources @Nullable - default MediaSource maybeBuildLiveMediaSource(@NonNull final PlayerDataSource dataSource, - @NonNull final StreamInfo info) { - final StreamType streamType = info.getStreamType(); - if (!StreamTypeUtil.isLiveStream(streamType)) { + static MediaSource maybeBuildLiveMediaSource(final PlayerDataSource dataSource, + final StreamInfo info) { + if (!StreamTypeUtil.isLiveStream(info.getStreamType())) { return null; } - final StreamInfoTag tag = StreamInfoTag.of(info); - if (!info.getHlsUrl().isEmpty()) { - return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag); - } else if (!info.getDashMpdUrl().isEmpty()) { - return buildLiveMediaSource(dataSource, info.getDashMpdUrl(), C.TYPE_DASH, tag); + try { + final StreamInfoTag tag = StreamInfoTag.of(info); + if (!info.getHlsUrl().isEmpty()) { + return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag); + } else if (!info.getDashMpdUrl().isEmpty()) { + return buildLiveMediaSource(dataSource, info.getDashMpdUrl(), C.TYPE_DASH, tag); + } + } catch (final Exception e) { + Log.w(TAG, "Error when generating live media source, falling back to standard sources", + e); } return null; } - @NonNull - default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSource, - @NonNull final String sourceUrl, - @C.ContentType final int type, - @NonNull final MediaItemTag metadata) { + static MediaSource buildLiveMediaSource(final PlayerDataSource dataSource, + final String sourceUrl, + @C.ContentType final int type, + final MediaItemTag metadata) throws ResolverException { final MediaSource.Factory factory; switch (type) { case C.TYPE_SS: @@ -56,8 +199,10 @@ public interface PlaybackResolver extends Resolver { case C.TYPE_HLS: factory = dataSource.getLiveHlsMediaSourceFactory(); break; + case C.TYPE_OTHER: + case C.TYPE_RTSP: default: - throw new IllegalStateException("Unsupported type: " + type); + throw new ResolverException("Unsupported type: " + type); } return factory.createMediaSource( @@ -67,46 +212,317 @@ public interface PlaybackResolver extends Resolver { .setLiveConfiguration( new MediaItem.LiveConfiguration.Builder() .setTargetOffsetMs(LIVE_STREAM_EDGE_GAP_MILLIS) - .build() - ) - .build() - ); + .build()) + .build()); } + //endregion - @NonNull - default MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource, - @NonNull final String sourceUrl, - @NonNull final String cacheKey, - @NonNull final String overrideExtension, - @NonNull final MediaItemTag metadata) { - final Uri uri = Uri.parse(sourceUrl); - @C.ContentType final int type = TextUtils.isEmpty(overrideExtension) - ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); - final MediaSource.Factory factory; - switch (type) { - case C.TYPE_SS: - factory = dataSource.getLiveSsMediaSourceFactory(); - break; - case C.TYPE_DASH: - factory = dataSource.getDashMediaSourceFactory(); - break; - case C.TYPE_HLS: - factory = dataSource.getHlsMediaSourceFactory(); - break; - case C.TYPE_OTHER: - factory = dataSource.getExtractorMediaSourceFactory(); - break; - default: - throw new IllegalStateException("Unsupported type: " + type); + //region Generic media sources + static MediaSource buildMediaSource(final PlayerDataSource dataSource, + final Stream stream, + final StreamInfo streamInfo, + final String cacheKey, + final MediaItemTag metadata) throws ResolverException { + if (streamInfo.getService() == ServiceList.YouTube) { + return createYoutubeMediaSource(stream, streamInfo, dataSource, cacheKey, metadata); } - return factory.createMediaSource( - new MediaItem.Builder() - .setTag(metadata) - .setUri(uri) - .setCustomCacheKey(cacheKey) - .build() - ); + final DeliveryMethod deliveryMethod = stream.getDeliveryMethod(); + switch (deliveryMethod) { + case PROGRESSIVE_HTTP: + return buildProgressiveMediaSource(dataSource, stream, cacheKey, metadata); + case DASH: + return buildDashMediaSource(dataSource, stream, cacheKey, metadata); + case HLS: + return buildHlsMediaSource(dataSource, stream, cacheKey, metadata); + case SS: + return buildSSMediaSource(dataSource, stream, cacheKey, metadata); + // Torrent streams are not supported by ExoPlayer + default: + throw new ResolverException("Unsupported delivery type: " + deliveryMethod); + } } + + private static ProgressiveMediaSource buildProgressiveMediaSource( + final PlayerDataSource dataSource, + final Stream stream, + final String cacheKey, + final MediaItemTag metadata) throws ResolverException { + if (!stream.isUrl()) { + throw new ResolverException("Non URI progressive contents are not supported"); + } + throwResolverExceptionIfUrlNullOrEmpty(stream.getContent()); + return dataSource.getProgressiveMediaSourceFactory().createMediaSource( + new MediaItem.Builder() + .setTag(metadata) + .setUri(Uri.parse(stream.getContent())) + .setCustomCacheKey(cacheKey) + .build()); + } + + private static DashMediaSource buildDashMediaSource(final PlayerDataSource dataSource, + final Stream stream, + final String cacheKey, + final MediaItemTag metadata) + throws ResolverException { + + if (stream.isUrl()) { + throwResolverExceptionIfUrlNullOrEmpty(stream.getContent()); + return dataSource.getDashMediaSourceFactory().createMediaSource( + new MediaItem.Builder() + .setTag(metadata) + .setUri(Uri.parse(stream.getContent())) + .setCustomCacheKey(cacheKey) + .build()); + } + + try { + return dataSource.getDashMediaSourceFactory().createMediaSource( + createDashManifest(stream.getContent(), stream), + new MediaItem.Builder() + .setTag(metadata) + .setUri(manifestUrlToUri(stream.getManifestUrl())) + .setCustomCacheKey(cacheKey) + .build()); + } catch (final IOException e) { + throw new ResolverException( + "Could not create a DASH media source/manifest from the manifest text", e); + } + } + + private static DashManifest createDashManifest(final String manifestContent, + final Stream stream) throws IOException { + return new DashManifestParser().parse(manifestUrlToUri(stream.getManifestUrl()), + new ByteArrayInputStream(manifestContent.getBytes(StandardCharsets.UTF_8))); + } + + private static HlsMediaSource buildHlsMediaSource(final PlayerDataSource dataSource, + final Stream stream, + final String cacheKey, + final MediaItemTag metadata) + throws ResolverException { + if (stream.isUrl()) { + throwResolverExceptionIfUrlNullOrEmpty(stream.getContent()); + return dataSource.getHlsMediaSourceFactory(null).createMediaSource( + new MediaItem.Builder() + .setTag(metadata) + .setUri(Uri.parse(stream.getContent())) + .setCustomCacheKey(cacheKey) + .build()); + } + + final NonUriHlsDataSourceFactory.Builder hlsDataSourceFactoryBuilder = + new NonUriHlsDataSourceFactory.Builder(); + hlsDataSourceFactoryBuilder.setPlaylistString(stream.getContent()); + + return dataSource.getHlsMediaSourceFactory(hlsDataSourceFactoryBuilder) + .createMediaSource(new MediaItem.Builder() + .setTag(metadata) + .setUri(manifestUrlToUri(stream.getManifestUrl())) + .setCustomCacheKey(cacheKey) + .build()); + } + + private static SsMediaSource buildSSMediaSource(final PlayerDataSource dataSource, + final Stream stream, + final String cacheKey, + final MediaItemTag metadata) + throws ResolverException { + if (stream.isUrl()) { + throwResolverExceptionIfUrlNullOrEmpty(stream.getContent()); + return dataSource.getSSMediaSourceFactory().createMediaSource( + new MediaItem.Builder() + .setTag(metadata) + .setUri(Uri.parse(stream.getContent())) + .setCustomCacheKey(cacheKey) + .build()); + } + + final Uri manifestUri = manifestUrlToUri(stream.getManifestUrl()); + + final SsManifest smoothStreamingManifest; + try { + final ByteArrayInputStream smoothStreamingManifestInput = new ByteArrayInputStream( + stream.getContent().getBytes(StandardCharsets.UTF_8)); + smoothStreamingManifest = new SsManifestParser().parse(manifestUri, + smoothStreamingManifestInput); + } catch (final IOException e) { + throw new ResolverException("Error when parsing manual SS manifest", e); + } + + return dataSource.getSSMediaSourceFactory().createMediaSource( + smoothStreamingManifest, + new MediaItem.Builder() + .setTag(metadata) + .setUri(manifestUri) + .setCustomCacheKey(cacheKey) + .build()); + } + //endregion + + + //region YouTube media sources + private static MediaSource createYoutubeMediaSource(final Stream stream, + final StreamInfo streamInfo, + final PlayerDataSource dataSource, + final String cacheKey, + final MediaItemTag metadata) + throws ResolverException { + if (!(stream instanceof AudioStream || stream instanceof VideoStream)) { + throw new ResolverException("Generation of YouTube DASH manifest for " + + stream.getClass().getSimpleName() + " is not supported"); + } + + final StreamType streamType = streamInfo.getStreamType(); + if (streamType == StreamType.VIDEO_STREAM) { + return createYoutubeMediaSourceOfVideoStreamType(dataSource, stream, streamInfo, + cacheKey, metadata); + } else if (streamType == StreamType.POST_LIVE_STREAM) { + // If the content is not an URL, uses the DASH delivery method and if the stream type + // of the stream is a post live stream, it means that the content is an ended + // livestream so we need to generate the manifest corresponding to the content + // (which is the last segment of the stream) + + try { + final ItagItem itagItem = Objects.requireNonNull(stream.getItagItem()); + final String manifestString = YoutubePostLiveStreamDvrDashManifestCreator + .fromPostLiveStreamDvrStreamingUrl(stream.getContent(), + itagItem, + itagItem.getTargetDurationSec(), + streamInfo.getDuration()); + return buildYoutubeManualDashMediaSource(dataSource, + createDashManifest(manifestString, stream), stream, cacheKey, + metadata); + } catch (final CreationException | IOException | NullPointerException e) { + throw new ResolverException( + "Error when generating the DASH manifest of YouTube ended live stream", e); + } + } else { + throw new ResolverException( + "DASH manifest generation of YouTube livestreams is not supported"); + } + } + + private static MediaSource createYoutubeMediaSourceOfVideoStreamType( + final PlayerDataSource dataSource, + final Stream stream, + final StreamInfo streamInfo, + final String cacheKey, + final MediaItemTag metadata) throws ResolverException { + final DeliveryMethod deliveryMethod = stream.getDeliveryMethod(); + switch (deliveryMethod) { + case PROGRESSIVE_HTTP: + if ((stream instanceof VideoStream && ((VideoStream) stream).isVideoOnly()) + || stream instanceof AudioStream) { + try { + final String manifestString = YoutubeProgressiveDashManifestCreator + .fromProgressiveStreamingUrl(stream.getContent(), + Objects.requireNonNull(stream.getItagItem()), + streamInfo.getDuration()); + return buildYoutubeManualDashMediaSource(dataSource, + createDashManifest(manifestString, stream), stream, cacheKey, + metadata); + } catch (final CreationException | IOException | NullPointerException e) { + Log.w(TAG, "Error when generating or parsing DASH manifest of " + + "YouTube progressive stream, falling back to a " + + "ProgressiveMediaSource.", e); + return buildYoutubeProgressiveMediaSource(dataSource, stream, cacheKey, + metadata); + } + } else { + // Legacy progressive streams, subtitles are handled by + // VideoPlaybackResolver + return buildYoutubeProgressiveMediaSource(dataSource, stream, cacheKey, + metadata); + } + case DASH: + // If the content is not a URL, uses the DASH delivery method and if the stream + // type of the stream is a video stream, it means the content is an OTF stream + // so we need to generate the manifest corresponding to the content (which is + // the base URL of the OTF stream). + + try { + final String manifestString = YoutubeOtfDashManifestCreator + .fromOtfStreamingUrl(stream.getContent(), + Objects.requireNonNull(stream.getItagItem()), + streamInfo.getDuration()); + return buildYoutubeManualDashMediaSource(dataSource, + createDashManifest(manifestString, stream), stream, cacheKey, + metadata); + } catch (final CreationException | IOException | NullPointerException e) { + Log.e(TAG, + "Error when generating the DASH manifest of YouTube OTF stream", e); + throw new ResolverException( + "Error when generating the DASH manifest of YouTube OTF stream", e); + } + case HLS: + return dataSource.getYoutubeHlsMediaSourceFactory().createMediaSource( + new MediaItem.Builder() + .setTag(metadata) + .setUri(Uri.parse(stream.getContent())) + .setCustomCacheKey(cacheKey) + .build()); + default: + throw new ResolverException("Unsupported delivery method for YouTube contents: " + + deliveryMethod); + } + } + + private static DashMediaSource buildYoutubeManualDashMediaSource( + final PlayerDataSource dataSource, + final DashManifest dashManifest, + final Stream stream, + final String cacheKey, + final MediaItemTag metadata) { + return dataSource.getYoutubeDashMediaSourceFactory().createMediaSource(dashManifest, + new MediaItem.Builder() + .setTag(metadata) + .setUri(Uri.parse(stream.getContent())) + .setCustomCacheKey(cacheKey) + .build()); + } + + private static ProgressiveMediaSource buildYoutubeProgressiveMediaSource( + final PlayerDataSource dataSource, + final Stream stream, + final String cacheKey, + final MediaItemTag metadata) { + return dataSource.getYoutubeProgressiveMediaSourceFactory() + .createMediaSource(new MediaItem.Builder() + .setTag(metadata) + .setUri(Uri.parse(stream.getContent())) + .setCustomCacheKey(cacheKey) + .build()); + } + //endregion + + + //region Utils + private static Uri manifestUrlToUri(final String manifestUrl) { + return Uri.parse(Objects.requireNonNullElse(manifestUrl, "")); + } + + private static void throwResolverExceptionIfUrlNullOrEmpty(@Nullable final String url) + throws ResolverException { + if (url == null) { + throw new ResolverException("Null stream URL"); + } else if (url.isEmpty()) { + throw new ResolverException("Empty stream URL"); + } + } + //endregion + + + //region Resolver exception + final class ResolverException extends Exception { + public ResolverException(final String message) { + super(message); + } + + public ResolverException(final String message, final Throwable cause) { + super(message, cause); + } + } + //endregion } diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java index 1aa7a5a18..cf7d73558 100644 --- a/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipe/player/resolver/VideoPlaybackResolver.java @@ -2,6 +2,7 @@ package org.schabi.newpipe.player.resolver; import android.content.Context; import android.net.Uri; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -27,8 +28,12 @@ import java.util.List; import java.util.Optional; import static com.google.android.exoplayer2.C.TIME_UNSET; +import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams; +import static org.schabi.newpipe.util.ListHelper.getNonTorrentStreams; public class VideoPlaybackResolver implements PlaybackResolver { + private static final String TAG = VideoPlaybackResolver.class.getSimpleName(); + @NonNull private final Context context; @NonNull @@ -57,7 +62,7 @@ public class VideoPlaybackResolver implements PlaybackResolver { @Override @Nullable public MediaSource resolve(@NonNull final StreamInfo info) { - final MediaSource liveSource = maybeBuildLiveMediaSource(dataSource, info); + final MediaSource liveSource = PlaybackResolver.maybeBuildLiveMediaSource(dataSource, info); if (liveSource != null) { streamSourceType = SourceType.LIVE_STREAM; return liveSource; @@ -66,40 +71,51 @@ public class VideoPlaybackResolver implements PlaybackResolver { final List mediaSources = new ArrayList<>(); // Create video stream source - final List videos = ListHelper.getSortedStreamVideosList(context, - info.getVideoStreams(), info.getVideoOnlyStreams(), false, true); + final List videoStreamsList = ListHelper.getSortedStreamVideosList(context, + getNonTorrentStreams(info.getVideoStreams()), + getNonTorrentStreams(info.getVideoOnlyStreams()), false, true); final int index; - if (videos.isEmpty()) { + if (videoStreamsList.isEmpty()) { index = -1; } else if (playbackQuality == null) { - index = qualityResolver.getDefaultResolutionIndex(videos); + index = qualityResolver.getDefaultResolutionIndex(videoStreamsList); } else { - index = qualityResolver.getOverrideResolutionIndex(videos, getPlaybackQuality()); + index = qualityResolver.getOverrideResolutionIndex(videoStreamsList, + getPlaybackQuality()); } - final MediaItemTag tag = StreamInfoTag.of(info, videos, index); + final MediaItemTag tag = StreamInfoTag.of(info, videoStreamsList, index); @Nullable final VideoStream video = tag.getMaybeQuality() .map(MediaItemTag.Quality::getSelectedVideoStream) .orElse(null); if (video != null) { - final MediaSource streamSource = buildMediaSource(dataSource, video.getUrl(), - PlayerHelper.cacheKeyOf(info, video), - MediaFormat.getSuffixById(video.getFormatId()), tag); - mediaSources.add(streamSource); + try { + final MediaSource streamSource = PlaybackResolver.buildMediaSource( + dataSource, video, info, PlaybackResolver.cacheKeyOf(info, video), tag); + mediaSources.add(streamSource); + } catch (final ResolverException e) { + Log.e(TAG, "Unable to create video source", e); + return null; + } } // Create optional audio stream source - final List audioStreams = info.getAudioStreams(); + final List audioStreams = getNonTorrentStreams(info.getAudioStreams()); final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get( ListHelper.getDefaultAudioFormat(context, audioStreams)); + // Use the audio stream if there is no video stream, or - // Merge with audio stream in case if video does not contain audio - if (audio != null && (video == null || video.isVideoOnly)) { - final MediaSource audioSource = buildMediaSource(dataSource, audio.getUrl(), - PlayerHelper.cacheKeyOf(info, audio), - MediaFormat.getSuffixById(audio.getFormatId()), tag); - mediaSources.add(audioSource); - streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO; + // merge with audio stream in case if video does not contain audio + if (audio != null && (video == null || video.isVideoOnly())) { + try { + final MediaSource audioSource = PlaybackResolver.buildMediaSource( + dataSource, audio, info, PlaybackResolver.cacheKeyOf(info, audio), tag); + mediaSources.add(audioSource); + streamSourceType = SourceType.VIDEO_WITH_SEPARATED_AUDIO; + } catch (final ResolverException e) { + Log.e(TAG, "Unable to create audio source", e); + return null; + } } else { streamSourceType = SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY; } @@ -108,36 +124,39 @@ public class VideoPlaybackResolver implements PlaybackResolver { if (mediaSources.isEmpty()) { return null; } + // Below are auxiliary media sources // Create subtitle sources - if (info.getSubtitles() != null) { - for (final SubtitlesStream subtitle : info.getSubtitles()) { - final String mimeType = PlayerHelper.subtitleMimeTypesOf(subtitle.getFormat()); - if (mimeType == null) { - continue; + final List subtitlesStreams = info.getSubtitles(); + if (subtitlesStreams != null) { + // Torrent and non URL subtitles are not supported by ExoPlayer + final List nonTorrentAndUrlStreams = getUrlAndNonTorrentStreams( + subtitlesStreams); + for (final SubtitlesStream subtitle : nonTorrentAndUrlStreams) { + final MediaFormat mediaFormat = subtitle.getFormat(); + if (mediaFormat != null) { + @C.RoleFlags final int textRoleFlag = subtitle.isAutoGenerated() + ? C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND + : C.ROLE_FLAG_CAPTION; + final MediaItem.SubtitleConfiguration textMediaItem = + new MediaItem.SubtitleConfiguration.Builder( + Uri.parse(subtitle.getContent())) + .setMimeType(mediaFormat.getMimeType()) + .setRoleFlags(textRoleFlag) + .setLanguage(PlayerHelper.captionLanguageOf(context, subtitle)) + .build(); + final MediaSource textSource = dataSource.getSingleSampleMediaSourceFactory() + .createMediaSource(textMediaItem, TIME_UNSET); + mediaSources.add(textSource); } - final @C.RoleFlags int textRoleFlag = subtitle.isAutoGenerated() - ? C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND - : C.ROLE_FLAG_CAPTION; - final MediaItem.SubtitleConfiguration textMediaItem = - new MediaItem.SubtitleConfiguration.Builder(Uri.parse(subtitle.getUrl())) - .setMimeType(mimeType) - .setRoleFlags(textRoleFlag) - .setLanguage(PlayerHelper.captionLanguageOf(context, subtitle)) - .build(); - final MediaSource textSource = dataSource - .getSampleMediaSourceFactory() - .createMediaSource(textMediaItem, TIME_UNSET); - mediaSources.add(textSource); } } if (mediaSources.size() == 1) { return mediaSources.get(0); } else { - return new MergingMediaSource(mediaSources.toArray( - new MediaSource[0])); + return new MergingMediaSource(true, mediaSources.toArray(new MediaSource[0])); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java index a3b131d75..3776d78f6 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java @@ -9,6 +9,7 @@ import androidx.annotation.NonNull; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.ReleaseVersionUtil; public class MainSettingsFragment extends BasePreferenceFragment { public static final boolean DEBUG = MainActivity.DEBUG; @@ -21,6 +22,14 @@ public class MainSettingsFragment extends BasePreferenceFragment { setHasOptionsMenu(true); // Otherwise onCreateOptionsMenu is not called + // Check if the app is updatable + if (!ReleaseVersionUtil.isReleaseApk()) { + getPreferenceScreen().removePreference( + findPreference(getString(R.string.update_pref_screen_key))); + + defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), false).apply(); + } + // Hide debug preferences in RELEASE build variant if (!DEBUG) { getPreferenceScreen().removePreference( diff --git a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java index c7eb0be40..1ff7947fd 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java @@ -207,7 +207,7 @@ public class PeertubeInstanceListFragment extends Fragment { new AlertDialog.Builder(c) .setTitle(R.string.peertube_instance_add_title) - .setIcon(R.drawable.place_holder_peertube) + .setIcon(R.drawable.ic_placeholder_peertube) .setView(dialogBinding.getRoot()) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.ok, (dialog1, which) -> { @@ -411,7 +411,7 @@ public class PeertubeInstanceListFragment extends Fragment { lastChecked = instanceRB; } }); - instanceIconView.setImageResource(R.drawable.place_holder_peertube); + instanceIconView.setImageResource(R.drawable.ic_placeholder_peertube); } @SuppressLint("ClickableViewAccessibility") diff --git a/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java index 62455d682..798d299c0 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java +++ b/app/src/main/java/org/schabi/newpipe/settings/custom/NotificationActionsPreference.java @@ -10,7 +10,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.TextView; @@ -21,11 +20,12 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.content.res.AppCompatResources; import androidx.core.graphics.drawable.DrawableCompat; -import androidx.core.widget.TextViewCompat; import androidx.preference.Preference; import androidx.preference.PreferenceViewHolder; import org.schabi.newpipe.R; +import org.schabi.newpipe.databinding.ListRadioIconItemBinding; +import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding; import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.NotificationConstants; import org.schabi.newpipe.util.DeviceUtils; @@ -190,13 +190,12 @@ public class NotificationActionsPreference extends Preference { void openActionChooserDialog() { final LayoutInflater inflater = LayoutInflater.from(getContext()); - final LinearLayout rootLayout = (LinearLayout) inflater.inflate( - R.layout.single_choice_dialog_view, null, false); - final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list); + final SingleChoiceDialogViewBinding binding = + SingleChoiceDialogViewBinding.inflate(inflater); final AlertDialog alertDialog = new AlertDialog.Builder(getContext()) .setTitle(SLOT_TITLES[i]) - .setView(radioGroup) + .setView(binding.getRoot()) .setCancelable(true) .create(); @@ -208,8 +207,8 @@ public class NotificationActionsPreference extends Preference { for (int id = 0; id < NotificationConstants.SLOT_ALLOWED_ACTIONS[i].length; ++id) { final int action = NotificationConstants.SLOT_ALLOWED_ACTIONS[i][id]; - final RadioButton radioButton - = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null); + final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater) + .getRoot(); // if present set action icon with correct color if (NotificationConstants.ACTION_ICONS[action] != 0) { @@ -220,8 +219,8 @@ public class NotificationActionsPreference extends Preference { android.R.attr.textColorPrimary); drawable = DrawableCompat.wrap(drawable).mutate(); DrawableCompat.setTint(drawable, color); - TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(radioButton, - null, null, drawable, null); + radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, + null, drawable, null); } } @@ -231,7 +230,7 @@ public class NotificationActionsPreference extends Preference { radioButton.setLayoutParams(new RadioGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); radioButton.setOnClickListener(radioButtonsClickListener); - radioGroup.addView(radioButton); + binding.list.addView(radioButton); } alertDialog.show(); diff --git a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java index 73aec4a7b..289c824ba 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/ChooseTabsFragment.java @@ -1,5 +1,8 @@ package org.schabi.newpipe.settings.tabs; +import static org.schabi.newpipe.settings.tabs.Tab.typeFrom; +import static org.schabi.newpipe.util.ServiceHelper.getNameOfServiceById; + import android.annotation.SuppressLint; import android.app.Dialog; import android.content.Context; @@ -28,7 +31,6 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.settings.SelectChannelFragment; import org.schabi.newpipe.settings.SelectKioskFragment; import org.schabi.newpipe.settings.SelectPlaylistFragment; @@ -39,8 +41,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import static org.schabi.newpipe.settings.tabs.Tab.typeFrom; - public class ChooseTabsFragment extends Fragment { private TabsManager tabsManager; @@ -374,36 +374,31 @@ public class ChooseTabsFragment extends Fragment { return; } - final String tabName; + tabNameView.setText(getTabName(type, tab)); + tabIconView.setImageResource(tab.getTabIconRes(requireContext())); + } + + private String getTabName(@NonNull final Tab.Type type, @NonNull final Tab tab) { switch (type) { case BLANK: - tabName = getString(R.string.blank_page_summary); - break; + return getString(R.string.blank_page_summary); case DEFAULT_KIOSK: - tabName = getString(R.string.default_kiosk_page_summary); - break; + return getString(R.string.default_kiosk_page_summary); case KIOSK: - tabName = NewPipe.getNameOfService(((Tab.KioskTab) tab) - .getKioskServiceId()) + "/" + tab.getTabName(requireContext()); - break; + return getNameOfServiceById(((Tab.KioskTab) tab).getKioskServiceId()) + + "/" + tab.getTabName(requireContext()); case CHANNEL: - tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab) - .getChannelServiceId()) + "/" + tab.getTabName(requireContext()); - break; + return getNameOfServiceById(((Tab.ChannelTab) tab).getChannelServiceId()) + + "/" + tab.getTabName(requireContext()); case PLAYLIST: final int serviceId = ((Tab.PlaylistTab) tab).getPlaylistServiceId(); final String serviceName = serviceId == -1 ? getString(R.string.local) - : NewPipe.getNameOfService(serviceId); - tabName = serviceName + "/" + tab.getTabName(requireContext()); - break; + : getNameOfServiceById(serviceId); + return serviceName + "/" + tab.getTabName(requireContext()); default: - tabName = tab.getTabName(requireContext()); - break; + return tab.getTabName(requireContext()); } - - tabNameView.setText(tabName); - tabIconView.setImageResource(tab.getTabIconRes(requireContext())); } @SuppressLint("ClickableViewAccessibility") diff --git a/app/src/main/java/org/schabi/newpipe/util/KeyboardUtil.java b/app/src/main/java/org/schabi/newpipe/util/KeyboardUtil.java index 71c0d3944..a709dc32e 100644 --- a/app/src/main/java/org/schabi/newpipe/util/KeyboardUtil.java +++ b/app/src/main/java/org/schabi/newpipe/util/KeyboardUtil.java @@ -24,7 +24,19 @@ public final class KeyboardUtil { if (editText.requestFocus()) { final InputMethodManager imm = ContextCompat.getSystemService(activity, InputMethodManager.class); - imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED); + if (!imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED)) { + /* + * Sometimes the keyboard can't be shown because Android's ImeFocusController is in + * a incorrect state e.g. when animations are disabled or the unfocus event of the + * previous view arrives in the wrong moment (see #7647 for details). + * The invalid state can be fixed by to re-focusing the editText. + */ + editText.clearFocus(); + editText.requestFocus(); + + // Try again + imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED); + } } } diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java index c3ccef87c..eabac8330 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -13,6 +13,8 @@ import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.DeliveryMethod; +import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.VideoStream; import java.util.ArrayList; @@ -24,6 +26,7 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; public final class ListHelper { @@ -37,10 +40,9 @@ public final class ListHelper { // Audio format in order of efficiency. 0=most efficient, n=least efficient private static final List AUDIO_FORMAT_EFFICIENCY_RANKING = Arrays.asList(MediaFormat.WEBMA, MediaFormat.M4A, MediaFormat.MP3); - - private static final Set HIGH_RESOLUTION_LIST - // Uses a HashSet for better performance - = new HashSet<>(Arrays.asList("1440p", "2160p", "1440p60", "2160p60")); + // Use a HashSet for better performance + private static final Set HIGH_RESOLUTION_LIST = new HashSet<>( + Arrays.asList("1440p", "2160p")); private ListHelper() { } @@ -110,6 +112,51 @@ public final class ListHelper { } } + /** + * Return a {@link Stream} list which uses the given delivery method from a {@link Stream} + * list. + * + * @param streamList the original {@link Stream stream} list + * @param deliveryMethod the {@link DeliveryMethod delivery method} + * @param the item type's class that extends {@link Stream} + * @return a {@link Stream stream} list which uses the given delivery method + */ + @NonNull + public static List getStreamsOfSpecifiedDelivery( + final List streamList, + final DeliveryMethod deliveryMethod) { + return getFilteredStreamList(streamList, + stream -> stream.getDeliveryMethod() == deliveryMethod); + } + + /** + * Return a {@link Stream} list which only contains URL streams and non-torrent streams. + * + * @param streamList the original stream list + * @param the item type's class that extends {@link Stream} + * @return a stream list which only contains URL streams and non-torrent streams + */ + @NonNull + public static List getUrlAndNonTorrentStreams( + final List streamList) { + return getFilteredStreamList(streamList, + stream -> stream.isUrl() && stream.getDeliveryMethod() != DeliveryMethod.TORRENT); + } + + /** + * Return a {@link Stream} list which only contains non-torrent streams. + * + * @param streamList the original stream list + * @param the item type's class that extends {@link Stream} + * @return a stream list which only contains non-torrent streams + */ + @NonNull + public static List getNonTorrentStreams( + final List streamList) { + return getFilteredStreamList(streamList, + stream -> stream.getDeliveryMethod() != DeliveryMethod.TORRENT); + } + /** * Join the two lists of video streams (video_only and normal videos), * and sort them according with default format chosen by the user. @@ -145,6 +192,26 @@ public final class ListHelper { // Utils //////////////////////////////////////////////////////////////////////////*/ + /** + * Get a filtered stream list, by using Java 8 Stream's API and the given predicate. + * + * @param streamList the stream list to filter + * @param streamListPredicate the predicate which will be used to filter streams + * @param the item type's class that extends {@link Stream} + * @return a new stream list filtered using the given predicate + */ + private static List getFilteredStreamList( + final List streamList, + final Predicate streamListPredicate) { + if (streamList == null) { + return Collections.emptyList(); + } + + return streamList.stream() + .filter(streamListPredicate) + .collect(Collectors.toList()); + } + private static String computeDefaultResolution(final Context context, final int key, final int value) { final SharedPreferences preferences @@ -177,7 +244,7 @@ public final class ListHelper { static int getDefaultResolutionIndex(final String defaultResolution, final String bestResolutionKey, final MediaFormat defaultFormat, - final List videoStreams) { + @Nullable final List videoStreams) { if (videoStreams == null || videoStreams.isEmpty()) { return -1; } @@ -233,7 +300,9 @@ public final class ListHelper { .flatMap(List::stream) // Filter out higher resolutions (or not if high resolutions should always be shown) .filter(stream -> showHigherResolutions - || !HIGH_RESOLUTION_LIST.contains(stream.getResolution())) + || !HIGH_RESOLUTION_LIST.contains(stream.getResolution() + // Replace any frame rate with nothing + .replaceAll("p\\d+$", "p"))) .collect(Collectors.toList()); final HashMap hashMap = new HashMap<>(); @@ -366,8 +435,9 @@ public final class ListHelper { * @param videoStreams the available video streams * @return the index of the preferred video stream */ - static int getVideoStreamIndex(final String targetResolution, final MediaFormat targetFormat, - final List videoStreams) { + static int getVideoStreamIndex(@NonNull final String targetResolution, + final MediaFormat targetFormat, + @NonNull final List videoStreams) { int fullMatchIndex = -1; int fullMatchNoRefreshIndex = -1; int resMatchOnlyIndex = -1; @@ -428,7 +498,7 @@ public final class ListHelper { * @param videoStreams the list of video streams to check * @return the index of the preferred video stream */ - private static int getDefaultResolutionWithDefaultFormat(final Context context, + private static int getDefaultResolutionWithDefaultFormat(@NonNull final Context context, final String defaultResolution, final List videoStreams) { final MediaFormat defaultFormat = getDefaultFormat(context, @@ -437,7 +507,7 @@ public final class ListHelper { context.getString(R.string.best_resolution_key), defaultFormat, videoStreams); } - private static MediaFormat getDefaultFormat(final Context context, + private static MediaFormat getDefaultFormat(@NonNull final Context context, @StringRes final int defaultFormatKey, @StringRes final int defaultFormatValueKey) { final SharedPreferences preferences @@ -457,8 +527,8 @@ public final class ListHelper { return defaultMediaFormat; } - private static MediaFormat getMediaFormatFromKey(final Context context, - final String formatKey) { + private static MediaFormat getMediaFormatFromKey(@NonNull final Context context, + @NonNull final String formatKey) { MediaFormat format = null; if (formatKey.equals(context.getString(R.string.video_webm_key))) { format = MediaFormat.WEBM; @@ -496,12 +566,20 @@ public final class ListHelper { - formatRanking.indexOf(streamB.getFormat()); } - private static int compareVideoStreamResolution(final String r1, final String r2) { - final int res1 = Integer.parseInt(r1.replaceAll("0p\\d+$", "1") - .replaceAll("[^\\d.]", "")); - final int res2 = Integer.parseInt(r2.replaceAll("0p\\d+$", "1") - .replaceAll("[^\\d.]", "")); - return res1 - res2; + private static int compareVideoStreamResolution(@NonNull final String r1, + @NonNull final String r2) { + try { + final int res1 = Integer.parseInt(r1.replaceAll("0p\\d+$", "1") + .replaceAll("[^\\d.]", "")); + final int res2 = Integer.parseInt(r2.replaceAll("0p\\d+$", "1") + .replaceAll("[^\\d.]", "")); + return res1 - res2; + } catch (final NumberFormatException e) { + // Consider the first one greater because we don't know if the two streams are + // different or not (a NumberFormatException was thrown so we don't know the resolution + // of one stream or of all streams) + return 1; + } } // Compares the quality of two video streams. @@ -536,7 +614,7 @@ public final class ListHelper { * @param context App context * @return maximum resolution allowed or null if there is no maximum */ - private static String getResolutionLimit(final Context context) { + private static String getResolutionLimit(@NonNull final Context context) { String resolutionLimit = null; if (isMeteredNetwork(context)) { final SharedPreferences preferences @@ -555,7 +633,7 @@ public final class ListHelper { * @param context App context * @return {@code true} if connected to a metered network */ - public static boolean isMeteredNetwork(final Context context) { + public static boolean isMeteredNetwork(@NonNull final Context context) { final ConnectivityManager manager = ContextCompat.getSystemService(context, ConnectivityManager.class); if (manager == null || manager.getActiveNetworkInfo() == null) { diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index e55114a2d..c40b1a430 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -33,6 +33,7 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.Stream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; @@ -60,7 +61,9 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.util.external_communication.ShareUtils; -import java.util.ArrayList; +import java.util.List; + +import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams; public final class NavigationHelper { public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag"; @@ -217,30 +220,47 @@ public final class NavigationHelper { public static void playOnExternalAudioPlayer(@NonNull final Context context, @NonNull final StreamInfo info) { - final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams()); - - if (index == -1) { + final List audioStreams = info.getAudioStreams(); + if (audioStreams == null || audioStreams.isEmpty()) { Toast.makeText(context, R.string.audio_streams_empty, Toast.LENGTH_SHORT).show(); return; } - final AudioStream audioStream = info.getAudioStreams().get(index); + final List audioStreamsForExternalPlayers = + getUrlAndNonTorrentStreams(audioStreams); + if (audioStreamsForExternalPlayers.isEmpty()) { + Toast.makeText(context, R.string.no_audio_streams_available_for_external_players, + Toast.LENGTH_SHORT).show(); + return; + } + + final int index = ListHelper.getDefaultAudioFormat(context, audioStreamsForExternalPlayers); + final AudioStream audioStream = audioStreamsForExternalPlayers.get(index); + playOnExternalPlayer(context, info.getName(), info.getUploaderName(), audioStream); } - public static void playOnExternalVideoPlayer(@NonNull final Context context, + public static void playOnExternalVideoPlayer(final Context context, @NonNull final StreamInfo info) { - final ArrayList videoStreamsList = new ArrayList<>( - ListHelper.getSortedStreamVideosList(context, info.getVideoStreams(), null, false, - false)); - final int index = ListHelper.getDefaultResolutionIndex(context, videoStreamsList); - - if (index == -1) { + final List videoStreams = info.getVideoStreams(); + if (videoStreams == null || videoStreams.isEmpty()) { Toast.makeText(context, R.string.video_streams_empty, Toast.LENGTH_SHORT).show(); return; } - final VideoStream videoStream = videoStreamsList.get(index); + final List videoStreamsForExternalPlayers = + ListHelper.getSortedStreamVideosList(context, + getUrlAndNonTorrentStreams(videoStreams), null, false, false); + if (videoStreamsForExternalPlayers.isEmpty()) { + Toast.makeText(context, R.string.no_video_streams_available_for_external_players, + Toast.LENGTH_SHORT).show(); + return; + } + + final int index = ListHelper.getDefaultResolutionIndex(context, + videoStreamsForExternalPlayers); + + final VideoStream videoStream = videoStreamsForExternalPlayers.get(index); playOnExternalPlayer(context, info.getName(), info.getUploaderName(), videoStream); } @@ -248,9 +268,48 @@ public final class NavigationHelper { @Nullable final String name, @Nullable final String artist, @NonNull final Stream stream) { + final DeliveryMethod deliveryMethod = stream.getDeliveryMethod(); + final String mimeType; + + if (!stream.isUrl() || deliveryMethod == DeliveryMethod.TORRENT) { + Toast.makeText(context, R.string.selected_stream_external_player_not_supported, + Toast.LENGTH_SHORT).show(); + return; + } + + switch (deliveryMethod) { + case PROGRESSIVE_HTTP: + if (stream.getFormat() == null) { + if (stream instanceof AudioStream) { + mimeType = "audio/*"; + } else if (stream instanceof VideoStream) { + mimeType = "video/*"; + } else { + // This should never be reached, because subtitles are not opened in + // external players + return; + } + } else { + mimeType = stream.getFormat().getMimeType(); + } + break; + case HLS: + mimeType = "application/x-mpegURL"; + break; + case DASH: + mimeType = "application/dash+xml"; + break; + case SS: + mimeType = "application/vnd.ms-sstr+xml"; + break; + default: + // Torrent streams are not exposed to external players + mimeType = ""; + } + final Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.parse(stream.getUrl()), stream.getFormat().getMimeType()); + intent.setDataAndType(Uri.parse(stream.getContent()), mimeType); intent.putExtra(Intent.EXTRA_TITLE, name); intent.putExtra("title", name); intent.putExtra("artist", artist); diff --git a/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java b/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java index da86ab1a4..aabc459d0 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java @@ -7,6 +7,8 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; +import androidx.annotation.Nullable; + import com.squareup.picasso.Cache; import com.squareup.picasso.LruCache; import com.squareup.picasso.OkHttp3Downloader; @@ -158,6 +160,11 @@ public final class PicassoHelper { }); } + @Nullable + public static Bitmap getImageFromCacheIfPresent(final String imageUrl) { + // URLs in the internal cache finish with \n so we need to add \n to image URLs + return picassoCache.get(imageUrl + "\n"); + } public static void loadNotificationIcon(final String url, final Consumer bitmapConsumer) { diff --git a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt index 21a9059e2..c07f307f2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt +++ b/app/src/main/java/org/schabi/newpipe/util/ReleaseVersionUtil.kt @@ -22,7 +22,7 @@ import java.time.format.DateTimeFormatter object ReleaseVersionUtil { // Public key of the certificate that is used in NewPipe release versions private const val RELEASE_CERT_PUBLIC_KEY_SHA1 = - "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15" + "7F:46:0D:D0:6A:2D:A0:6B:57:B5:2C:ED:73:06:B7:87:43:90:66:A9" @JvmStatic fun isReleaseApk(): Boolean { diff --git a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java index 8c697d327..e7fd2d4a4 100644 --- a/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/SecondaryStreamHelper.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.util; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.AudioStream; @@ -14,7 +15,8 @@ public class SecondaryStreamHelper { private final int position; private final StreamSizeWrapper streams; - public SecondaryStreamHelper(final StreamSizeWrapper streams, final T selectedStream) { + public SecondaryStreamHelper(@NonNull final StreamSizeWrapper streams, + final T selectedStream) { this.streams = streams; this.position = streams.getStreamsList().indexOf(selectedStream); if (this.position < 0) { @@ -29,9 +31,15 @@ public class SecondaryStreamHelper { * @param videoStream desired video ONLY stream * @return selected audio stream or null if a candidate was not found */ + @Nullable public static AudioStream getAudioStreamFor(@NonNull final List audioStreams, @NonNull final VideoStream videoStream) { - switch (videoStream.getFormat()) { + final MediaFormat mediaFormat = videoStream.getFormat(); + if (mediaFormat == null) { + return null; + } + + switch (mediaFormat) { case WEBM: case MPEG_4:// ¿is mpeg-4 DASH? break; @@ -39,7 +47,7 @@ public class SecondaryStreamHelper { return null; } - final boolean m4v = videoStream.getFormat() == MediaFormat.MPEG_4; + final boolean m4v = (mediaFormat == MediaFormat.MPEG_4); for (final AudioStream audio : audioStreams) { if (audio.getFormat() == (m4v ? MediaFormat.M4A : MediaFormat.WEBMA)) { diff --git a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java index d41493a7f..b13ae4a97 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java @@ -1,9 +1,13 @@ package org.schabi.newpipe.util; +import static org.schabi.newpipe.extractor.ServiceList.SoundCloud; + import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.preference.PreferenceManager; @@ -18,10 +22,9 @@ import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance; +import java.util.Optional; import java.util.concurrent.TimeUnit; -import static org.schabi.newpipe.extractor.ServiceList.SoundCloud; - public final class ServiceHelper { private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube; @@ -31,17 +34,17 @@ public final class ServiceHelper { public static int getIcon(final int serviceId) { switch (serviceId) { case 0: - return R.drawable.place_holder_youtube; + return R.drawable.ic_smart_display; case 1: - return R.drawable.place_holder_cloud; + return R.drawable.ic_cloud; case 2: - return R.drawable.place_holder_gadse; + return R.drawable.ic_placeholder_media_ccc; case 3: - return R.drawable.place_holder_peertube; + return R.drawable.ic_placeholder_peertube; case 4: - return R.drawable.place_holder_bandcamp; + return R.drawable.ic_placeholder_bandcamp; default: - return R.drawable.place_holder_circle; + return R.drawable.ic_circle; } } @@ -113,18 +116,32 @@ public final class ServiceHelper { } public static int getSelectedServiceId(final Context context) { + return Optional.ofNullable(getSelectedService(context)) + .orElse(DEFAULT_FALLBACK_SERVICE) + .getServiceId(); + } + + @Nullable + public static StreamingService getSelectedService(final Context context) { final String serviceName = PreferenceManager.getDefaultSharedPreferences(context) .getString(context.getString(R.string.current_service_key), context.getString(R.string.default_service_value)); - int serviceId; try { - serviceId = NewPipe.getService(serviceName).getServiceId(); + return NewPipe.getService(serviceName); } catch (final ExtractionException e) { - serviceId = DEFAULT_FALLBACK_SERVICE.getServiceId(); + return null; } + } - return serviceId; + @NonNull + public static String getNameOfServiceById(final int serviceId) { + return ServiceList.all().stream() + .filter(s -> s.getServiceId() == serviceId) + .findFirst() + .map(StreamingService::getServiceInfo) + .map(StreamingService.ServiceInfo::getName) + .orElse(""); } public static void setSelectedServiceId(final Context context, final int serviceId) { @@ -138,16 +155,6 @@ public final class ServiceHelper { setSelectedServicePreferences(context, serviceName); } - public static void setSelectedServiceId(final Context context, final String serviceName) { - final int serviceId = NewPipe.getIdOfService(serviceName); - if (serviceId == -1) { - setSelectedServicePreferences(context, - DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName()); - } else { - setSelectedServicePreferences(context, serviceName); - } - } - private static void setSelectedServicePreferences(final Context context, final String serviceName) { PreferenceManager.getDefaultSharedPreferences(context).edit(). diff --git a/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java b/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java index b8cd4ef69..0c5f418b2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java +++ b/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java @@ -1,7 +1,5 @@ package org.schabi.newpipe.util; -import static org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM; -import static org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; import android.content.Context; @@ -49,8 +47,8 @@ public final class SparseItemUtil { public static void fetchItemInfoIfSparse(@NonNull final Context context, @NonNull final StreamInfoItem item, @NonNull final Consumer callback) { - if (((item.getStreamType() == LIVE_STREAM || item.getStreamType() == AUDIO_LIVE_STREAM) - || item.getDuration() >= 0) && !isNullOrEmpty(item.getUploaderUrl())) { + if ((StreamTypeUtil.isLiveStream(item.getStreamType()) || item.getDuration() >= 0) + && !isNullOrEmpty(item.getUploaderUrl())) { // if the duration is >= 0 (provided that the item is not a livestream) and there is an // uploader url, probably all info is already there, so there is no need to fetch it callback.accept(new SinglePlayQueue(item)); diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java index 03342a497..4b5e675c9 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java @@ -10,6 +10,8 @@ import android.widget.ImageView; import android.widget.Spinner; import android.widget.TextView; +import androidx.annotation.NonNull; + import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; @@ -87,7 +89,8 @@ public class StreamItemAdapter extends BaseA } @Override - public View getDropDownView(final int position, final View convertView, + public View getDropDownView(final int position, + final View convertView, final ViewGroup parent) { return getCustomView(position, convertView, parent, true); } @@ -98,7 +101,10 @@ public class StreamItemAdapter extends BaseA convertView, parent, false); } - private View getCustomView(final int position, final View view, final ViewGroup parent, + @NonNull + private View getCustomView(final int position, + final View view, + final ViewGroup parent, final boolean isDropdownItem) { View convertView = view; if (convertView == null) { @@ -112,6 +118,7 @@ public class StreamItemAdapter extends BaseA final TextView sizeView = convertView.findViewById(R.id.stream_size); final T stream = getItem(position); + final MediaFormat mediaFormat = stream.getFormat(); int woSoundIconVisibility = View.GONE; String qualityString; @@ -135,24 +142,32 @@ public class StreamItemAdapter extends BaseA } } else if (stream instanceof AudioStream) { final AudioStream audioStream = ((AudioStream) stream); - qualityString = audioStream.getAverageBitrate() > 0 - ? audioStream.getAverageBitrate() + "kbps" - : audioStream.getFormat().getName(); + if (audioStream.getAverageBitrate() > 0) { + qualityString = audioStream.getAverageBitrate() + "kbps"; + } else if (mediaFormat != null) { + qualityString = mediaFormat.getName(); + } else { + qualityString = context.getString(R.string.unknown_quality); + } } else if (stream instanceof SubtitlesStream) { qualityString = ((SubtitlesStream) stream).getDisplayLanguageName(); if (((SubtitlesStream) stream).isAutoGenerated()) { qualityString += " (" + context.getString(R.string.caption_auto_generated) + ")"; } } else { - qualityString = stream.getFormat().getSuffix(); + if (mediaFormat == null) { + qualityString = context.getString(R.string.unknown_quality); + } else { + qualityString = mediaFormat.getSuffix(); + } } if (streamsWrapper.getSizeInBytes(position) > 0) { final SecondaryStreamHelper secondary = secondaryStreams == null ? null : secondaryStreams.get(position); if (secondary != null) { - final long size - = secondary.getSizeInBytes() + streamsWrapper.getSizeInBytes(position); + final long size = secondary.getSizeInBytes() + + streamsWrapper.getSizeInBytes(position); sizeView.setText(Utility.formatBytes(size)); } else { sizeView.setText(streamsWrapper.getFormattedSize(position)); @@ -164,11 +179,15 @@ public class StreamItemAdapter extends BaseA if (stream instanceof SubtitlesStream) { formatNameView.setText(((SubtitlesStream) stream).getLanguageTag()); - } else if (stream.getFormat() == MediaFormat.WEBMA_OPUS) { - // noinspection AndroidLintSetTextI18n - formatNameView.setText("opus"); } else { - formatNameView.setText(stream.getFormat().getName()); + if (mediaFormat == null) { + formatNameView.setText(context.getString(R.string.unknown_format)); + } else if (mediaFormat == MediaFormat.WEBMA_OPUS) { + // noinspection AndroidLintSetTextI18n + formatNameView.setText("opus"); + } else { + formatNameView.setText(mediaFormat.getName()); + } } qualityView.setText(qualityString); @@ -233,6 +252,7 @@ public class StreamItemAdapter extends BaseA * @param streamsWrapper the wrapper * @return a {@link Single} that returns a boolean indicating if any elements were changed */ + @NonNull public static Single fetchSizeForWrapper( final StreamSizeWrapper streamsWrapper) { final Callable fetchAndSet = () -> { @@ -243,7 +263,7 @@ public class StreamItemAdapter extends BaseA } final long contentLength = DownloaderImpl.getInstance().getContentLength( - stream.getUrl()); + stream.getContent()); streamsWrapper.setSize(stream, contentLength); hasChanged = true; } diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamTypeUtil.java b/app/src/main/java/org/schabi/newpipe/util/StreamTypeUtil.java index 87b3eed4f..0cc0ecf1f 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamTypeUtil.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamTypeUtil.java @@ -3,7 +3,7 @@ package org.schabi.newpipe.util; import org.schabi.newpipe.extractor.stream.StreamType; /** - * Utility class for {@link org.schabi.newpipe.extractor.stream.StreamType}. + * Utility class for {@link StreamType}. */ public final class StreamTypeUtil { private StreamTypeUtil() { @@ -11,11 +11,37 @@ public final class StreamTypeUtil { } /** - * Checks if the streamType is a livestream. + * Check if the {@link StreamType} of a stream is a livestream. * - * @param streamType - * @return true when the streamType is a - * {@link StreamType#LIVE_STREAM} or {@link StreamType#AUDIO_LIVE_STREAM} + * @param streamType the stream type of the stream + * @return whether the stream type is {@link StreamType#AUDIO_STREAM}, + * {@link StreamType#AUDIO_LIVE_STREAM} or {@link StreamType#POST_LIVE_AUDIO_STREAM} + */ + public static boolean isAudio(final StreamType streamType) { + return streamType == StreamType.AUDIO_STREAM + || streamType == StreamType.AUDIO_LIVE_STREAM + || streamType == StreamType.POST_LIVE_AUDIO_STREAM; + } + + /** + * Check if the {@link StreamType} of a stream is a livestream. + * + * @param streamType the stream type of the stream + * @return whether the stream type is {@link StreamType#VIDEO_STREAM}, + * {@link StreamType#LIVE_STREAM} or {@link StreamType#POST_LIVE_STREAM} + */ + public static boolean isVideo(final StreamType streamType) { + return streamType == StreamType.VIDEO_STREAM + || streamType == StreamType.LIVE_STREAM + || streamType == StreamType.POST_LIVE_STREAM; + } + + /** + * Check if the {@link StreamType} of a stream is a livestream. + * + * @param streamType the stream type of the stream + * @return whether the stream type is {@link StreamType#LIVE_STREAM} or + * {@link StreamType#AUDIO_LIVE_STREAM} */ public static boolean isLiveStream(final StreamType streamType) { return streamType == StreamType.LIVE_STREAM diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java index 7c47d387f..b8e3a86ed 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java @@ -23,14 +23,17 @@ import android.app.Activity; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.drawable.Drawable; import android.util.TypedValue; import androidx.annotation.AttrRes; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StyleRes; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.content.res.AppCompatResources; import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; @@ -227,6 +230,20 @@ public final class ThemeHelper { return value.data; } + /** + * Resolves a {@link Drawable} by it's id. + * + * @param context Context + * @param attrResId Resource id + * @return the {@link Drawable} + */ + public static Drawable resolveDrawable(@NonNull final Context context, + @AttrRes final int attrResId) { + final TypedValue typedValue = new TypedValue(); + context.getTheme().resolveAttribute(attrResId, typedValue, true); + return AppCompatResources.getDrawable(context, typedValue.resourceId); + } + private static String getSelectedThemeKey(final Context context) { final String themeKey = context.getString(R.string.theme_key); final String defaultTheme = context.getResources().getString(R.string.default_theme_value); diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java index c4f1675cf..8324146fe 100644 --- a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java @@ -1,5 +1,7 @@ package org.schabi.newpipe.util.external_communication; +import static org.schabi.newpipe.MainActivity.DEBUG; + import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.ClipboardManager; @@ -7,17 +9,28 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; import android.text.TextUtils; +import android.util.Log; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; +import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; +import org.schabi.newpipe.util.PicassoHelper; + +import java.io.File; +import java.io.FileOutputStream; public final class ShareUtils { + private static final String TAG = ShareUtils.class.getSimpleName(); + private ShareUtils() { } @@ -231,9 +244,11 @@ public final class ShareUtils { /** * Open the android share sheet to share a content. * + *

* For Android 10+ users, a content preview is shown, which includes the title of the shared - * content. - * Support sharing the image of the content needs to done, if possible. + * content and an image preview the content, if its URL is not null or empty and its + * corresponding image is in the image cache. + *

* * @param context the context to use * @param title the title of the content @@ -252,13 +267,20 @@ public final class ShareUtils { shareIntent.putExtra(Intent.EXTRA_SUBJECT, title); } - /* TODO: add the image of the content to Android share sheet with setClipData after - generating a content URI of this image, then use ClipData.newUri(the content resolver, - null, the content URI) and set the ClipData to the share intent with - shareIntent.setClipData(generated ClipData). - if (!imagePreviewUrl.isEmpty()) { - //shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - }*/ + // Content preview in the share sheet has been added in Android 10, so it's not needed to + // set a content preview which will be never displayed + // See https://developer.android.com/training/sharing/send#adding-rich-content-previews + // If loading of images has been disabled, don't try to generate a content preview + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + && !TextUtils.isEmpty(imagePreviewUrl) + && PicassoHelper.getShouldLoadImages()) { + + final ClipData clipData = generateClipDataForImagePreview(context, imagePreviewUrl); + if (clipData != null) { + shareIntent.setClipData(clipData); + shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + } openAppChooser(context, shareIntent, false); } @@ -266,11 +288,11 @@ public final class ShareUtils { /** * Open the android share sheet to share a content. * - * For Android 10+ users, a content preview is shown, which includes the title of the shared - * content. *

* This calls {@link #shareText(Context, String, String, String)} with an empty string for the - * imagePreviewUrl parameter. + * {@code imagePreviewUrl} parameter. This method should be used when the shared content has no + * preview thumbnail. + *

* * @param context the context to use * @param title the title of the content @@ -301,4 +323,81 @@ public final class ShareUtils { clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text)); Toast.makeText(context, R.string.msg_copied, Toast.LENGTH_SHORT).show(); } + + /** + * Generate a {@link ClipData} with the image of the content shared, if it's in the app cache. + * + *

+ * In order not to worry about network issues (timeouts, DNS issues, low connection speed, ...) + * when sharing a content, only images in the {@link com.squareup.picasso.LruCache LruCache} + * used by the Picasso library inside {@link PicassoHelper} are used as preview images. If the + * thumbnail image is not in the cache, no {@link ClipData} will be generated and {@code null} + * will be returned. + *

+ * + *

+ * In order to display the image in the content preview of the Android share sheet, an URI of + * the content, accessible and readable by other apps has to be generated, so a new file inside + * the application cache will be generated, named {@code android_share_sheet_image_preview.jpg} + * (if a file under this name already exists, it will be overwritten). The thumbnail will be + * compressed in JPEG format, with a {@code 90} compression level. + *

+ * + *

+ * Note that if an exception occurs when generating the {@link ClipData}, {@code null} is + * returned. + *

+ * + *

+ * This method will call {@link PicassoHelper#getImageFromCacheIfPresent(String)} to get the + * thumbnail of the content in the {@link com.squareup.picasso.LruCache LruCache} used by + * the Picasso library inside {@link PicassoHelper}. + *

+ * + *

+ * Using the result of this method when sharing has only an effect on the system share sheet (if + * OEMs didn't change Android system standard behavior) on Android API 29 and higher. + *

+ * + * @param context the context to use + * @param thumbnailUrl the URL of the content thumbnail + * @return a {@link ClipData} of the content thumbnail, or {@code null} + */ + @Nullable + private static ClipData generateClipDataForImagePreview( + @NonNull final Context context, + @NonNull final String thumbnailUrl) { + try { + final Bitmap bitmap = PicassoHelper.getImageFromCacheIfPresent(thumbnailUrl); + if (bitmap == null) { + return null; + } + + // Save the image in memory to the application's cache because we need a URI to the + // image to generate a ClipData which will show the share sheet, and so an image file + final Context applicationContext = context.getApplicationContext(); + final String appFolder = applicationContext.getCacheDir().getAbsolutePath(); + final File thumbnailPreviewFile = new File(appFolder + + "/android_share_sheet_image_preview.jpg"); + + // Any existing file will be overwritten with FileOutputStream + final FileOutputStream fileOutputStream = new FileOutputStream(thumbnailPreviewFile); + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fileOutputStream); + fileOutputStream.close(); + + final ClipData clipData = ClipData.newUri(applicationContext.getContentResolver(), "", + FileProvider.getUriForFile(applicationContext, + BuildConfig.APPLICATION_ID + ".provider", + thumbnailPreviewFile)); + + if (DEBUG) { + Log.d(TAG, "ClipData successfully generated for Android share sheet: " + clipData); + } + return clipData; + + } catch (final Exception e) { + Log.w(TAG, "Error when setting preview image for share sheet", e); + return null; + } + } } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java b/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java index 90886b63c..e001c6f3f 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java @@ -6,6 +6,7 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.DeliveryMethod; import org.schabi.newpipe.extractor.stream.StreamExtractor; import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.VideoStream; @@ -131,31 +132,38 @@ public class DownloadMissionRecover extends Thread { switch (mRecovery.getKind()) { case 'a': - for (AudioStream audio : mExtractor.getAudioStreams()) { - if (audio.getAverageBitrate() == mRecovery.getDesiredBitrate() && audio.getFormat() == mRecovery.getFormat()) { - url = audio.getUrl(); + for (final AudioStream audio : mExtractor.getAudioStreams()) { + if (audio.getAverageBitrate() == mRecovery.getDesiredBitrate() + && audio.getFormat() == mRecovery.getFormat() + && audio.getDeliveryMethod() == DeliveryMethod.PROGRESSIVE_HTTP) { + url = audio.getContent(); break; } } break; case 'v': - List videoStreams; + final List videoStreams; if (mRecovery.isDesired2()) videoStreams = mExtractor.getVideoOnlyStreams(); else videoStreams = mExtractor.getVideoStreams(); - for (VideoStream video : videoStreams) { - if (video.resolution.equals(mRecovery.getDesired()) && video.getFormat() == mRecovery.getFormat()) { - url = video.getUrl(); + for (final VideoStream video : videoStreams) { + if (video.getResolution().equals(mRecovery.getDesired()) + && video.getFormat() == mRecovery.getFormat() + && video.getDeliveryMethod() == DeliveryMethod.PROGRESSIVE_HTTP) { + url = video.getContent(); break; } } break; case 's': - for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.getFormat())) { + for (final SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery + .getFormat())) { String tag = subtitles.getLanguageTag(); - if (tag.equals(mRecovery.getDesired()) && subtitles.isAutoGenerated() == mRecovery.isDesired2()) { - url = subtitles.getUrl(); + if (tag.equals(mRecovery.getDesired()) + && subtitles.isAutoGenerated() == mRecovery.isDesired2() + && subtitles.getDeliveryMethod() == DeliveryMethod.PROGRESSIVE_HTTP) { + url = subtitles.getContent(); break; } } diff --git a/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.kt b/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.kt index 11293a610..c2f9dc9b2 100644 --- a/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.kt +++ b/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.kt @@ -11,23 +11,23 @@ import java.io.Serializable @Parcelize class MissionRecoveryInfo( - var format: MediaFormat, + var format: MediaFormat?, var desired: String? = null, var isDesired2: Boolean = false, var desiredBitrate: Int = 0, var kind: Char = Char.MIN_VALUE, var validateCondition: String? = null ) : Serializable, Parcelable { - constructor(stream: Stream) : this(format = stream.getFormat()!!) { + constructor(stream: Stream) : this(format = stream.format) { when (stream) { is AudioStream -> { - desiredBitrate = stream.averageBitrate + desiredBitrate = stream.getAverageBitrate() isDesired2 = false kind = 'a' } is VideoStream -> { - desired = stream.resolution - isDesired2 = stream.isVideoOnly + desired = stream.getResolution() + isDesired2 = stream.isVideoOnly() kind = 'v' } is SubtitlesStream -> { @@ -62,7 +62,7 @@ class MissionRecoveryInfo( } } str.append(" format=") - .append(format.getName()) + .append(format?.getName()) .append(' ') .append(info) .append('}') diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index abbead5d6..aac88ea56 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -30,7 +30,6 @@ import androidx.appcompat.app.AlertDialog; import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; -import androidx.core.view.ViewCompat; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; @@ -900,7 +899,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb super(view); progress = new ProgressDrawable(); - ViewCompat.setBackground(itemView.findViewById(R.id.item_bkg), progress); + itemView.findViewById(R.id.item_bkg).setBackground(progress); status = itemView.findViewById(R.id.item_status); name = itemView.findViewById(R.id.item_name); diff --git a/app/src/main/res/drawable-nodpi/place_holder_bandcamp.png b/app/src/main/res/drawable-nodpi/place_holder_bandcamp.png deleted file mode 100644 index 13c44b649..000000000 Binary files a/app/src/main/res/drawable-nodpi/place_holder_bandcamp.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/place_holder_circle.png b/app/src/main/res/drawable-nodpi/place_holder_circle.png deleted file mode 100644 index 630d0454e..000000000 Binary files a/app/src/main/res/drawable-nodpi/place_holder_circle.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/place_holder_cloud.png b/app/src/main/res/drawable-nodpi/place_holder_cloud.png deleted file mode 100644 index c4ba2a6f4..000000000 Binary files a/app/src/main/res/drawable-nodpi/place_holder_cloud.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/place_holder_gadse.png b/app/src/main/res/drawable-nodpi/place_holder_gadse.png deleted file mode 100644 index 9b479ed4f..000000000 Binary files a/app/src/main/res/drawable-nodpi/place_holder_gadse.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/place_holder_peertube.png b/app/src/main/res/drawable-nodpi/place_holder_peertube.png deleted file mode 100644 index 81dfdb8cc..000000000 Binary files a/app/src/main/res/drawable-nodpi/place_holder_peertube.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/place_holder_youtube.png b/app/src/main/res/drawable-nodpi/place_holder_youtube.png deleted file mode 100644 index d147c6643..000000000 Binary files a/app/src/main/res/drawable-nodpi/place_holder_youtube.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_circle.xml b/app/src/main/res/drawable/ic_circle.xml new file mode 100644 index 000000000..dc0a218b8 --- /dev/null +++ b/app/src/main/res/drawable/ic_circle.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_cloud.xml b/app/src/main/res/drawable/ic_cloud.xml new file mode 100644 index 000000000..15a682b76 --- /dev/null +++ b/app/src/main/res/drawable/ic_cloud.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_placeholder_bandcamp.xml b/app/src/main/res/drawable/ic_placeholder_bandcamp.xml new file mode 100644 index 000000000..411e69854 --- /dev/null +++ b/app/src/main/res/drawable/ic_placeholder_bandcamp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_placeholder_media_ccc.xml b/app/src/main/res/drawable/ic_placeholder_media_ccc.xml new file mode 100644 index 000000000..cdc743cb2 --- /dev/null +++ b/app/src/main/res/drawable/ic_placeholder_media_ccc.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_placeholder_peertube.xml b/app/src/main/res/drawable/ic_placeholder_peertube.xml new file mode 100644 index 000000000..263d92d70 --- /dev/null +++ b/app/src/main/res/drawable/ic_placeholder_peertube.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_smart_display.xml b/app/src/main/res/drawable/ic_smart_display.xml new file mode 100644 index 000000000..d666a3b37 --- /dev/null +++ b/app/src/main/res/drawable/ic_smart_display.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/dialog_playback_parameter.xml b/app/src/main/res/layout/dialog_playback_parameter.xml index 862b2ea67..e402f4fb1 100644 --- a/app/src/main/res/layout/dialog_playback_parameter.xml +++ b/app/src/main/res/layout/dialog_playback_parameter.xml @@ -1,5 +1,6 @@ @@ -117,10 +111,8 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_alignParentEnd="true" - android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginEnd="4dp" - android:layout_marginRight="4dp" android:background="?attr/selectableItemBackground" android:clickable="true" android:focusable="true" @@ -138,9 +130,9 @@ android:layout_height="1dp" android:layout_below="@id/tempoControl" android:layout_marginStart="12dp" - android:layout_marginTop="6dp" - android:layout_marginEnd="6dp" - android:layout_marginBottom="6dp" + android:layout_marginTop="5dp" + android:layout_marginEnd="12dp" + android:layout_marginBottom="5dp" android:background="?attr/separator_color" /> - + + + android:layout_marginStart="22dp" + android:layout_marginEnd="22dp" + android:orientation="horizontal" + android:visibility="gone" + tools:visibility="visible"> + android:text="@string/percent" + android:textColor="?attr/colorAccent" /> + + + + + + - - + tools:text="-5%" /> + + + + + + + + + + + - - + tools:text="+5%" /> - - - - - - - + android:orientation="horizontal" + tools:visibility="gone"> - - - - - - + android:layout_height="match_parent" + android:layout_marginLeft="4dp" + android:layout_marginRight="4dp" + android:layout_toStartOf="@+id/pitchSemitoneStepUp" + android:layout_toEndOf="@+id/pitchSemitoneStepDown" + android:orientation="horizontal"> - + + + + + + + + + + + @@ -403,7 +431,8 @@ android:clickable="true" android:focusable="true" android:gravity="center" - android:textColor="?attr/colorAccent" /> + android:textColor="?attr/colorAccent" + tools:text="1%" /> + android:textColor="?attr/colorAccent" + tools:text="5%" /> + android:textColor="?attr/colorAccent" + tools:text="10%" /> + android:textColor="?attr/colorAccent" + tools:text="25%" /> - + android:textColor="?attr/colorAccent" + tools:text="100%" /> - diff --git a/app/src/main/res/layout/download_dialog.xml b/app/src/main/res/layout/download_dialog.xml index 33e18c64a..37bbf2b03 100644 --- a/app/src/main/res/layout/download_dialog.xml +++ b/app/src/main/res/layout/download_dialog.xml @@ -82,13 +82,14 @@ android:text="@string/msg_threads" /> + android:layout_marginBottom="12dp" + android:orientation="horizontal"> + + + diff --git a/app/src/main/res/layout/drawer_header.xml b/app/src/main/res/layout/drawer_header.xml index d2e936870..94e045863 100644 --- a/app/src/main/res/layout/drawer_header.xml +++ b/app/src/main/res/layout/drawer_header.xml @@ -86,7 +86,7 @@ android:scaleType="fitCenter" app:tint="@color/drawer_header_font_color" tools:ignore="ContentDescription" - tools:srcCompat="@drawable/place_holder_youtube" /> + tools:srcCompat="@drawable/ic_smart_display" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_instance.xml b/app/src/main/res/layout/item_instance.xml index 1a96c5bb2..dd5b4156f 100644 --- a/app/src/main/res/layout/item_instance.xml +++ b/app/src/main/res/layout/item_instance.xml @@ -26,7 +26,7 @@ android:layout_centerVertical="true" android:layout_marginLeft="10dp" tools:ignore="ContentDescription,RtlHardcoded" - tools:src="@drawable/place_holder_peertube" /> + tools:src="@drawable/ic_placeholder_peertube" /> - + + android:layout_height="match_parent" + android:fadeScrollbars="false"> + + + + + diff --git a/app/src/main/res/layout/subscription_import_export_item.xml b/app/src/main/res/layout/subscription_import_export_item.xml deleted file mode 100644 index 8aadf5d8c..000000000 --- a/app/src/main/res/layout/subscription_import_export_item.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/menu/menu_playlist.xml b/app/src/main/res/menu/menu_playlist.xml index 8e3ea1559..91ec1bc94 100644 --- a/app/src/main/res/menu/menu_playlist.xml +++ b/app/src/main/res/menu/menu_playlist.xml @@ -28,4 +28,10 @@ android:orderInCategory="2" android:title="@string/open_in_browser" app:showAsAction="never" /> + + diff --git a/app/src/main/res/values-ang/strings.xml b/app/src/main/res/values-ang/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/main/res/values-ang/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 3bb304848..4e9606f48 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -23,7 +23,7 @@ فاتح خطأ في الشبكة لم يتم العثور على مشغل بث. تثبيت VLC؟ - افتح في المتصفح + فتح في المتصفح الصوت تشغيل بواسطة كودي البحث @@ -730,11 +730,8 @@ إظهار خطأ snackbar لم يتم العثور على مدير ملفات مناسب لهذا الإجراء. \nالرجاء تثبيت مدير ملفات متوافق مع Storage Access Framework. - يتم تشغيله في الخلفية تعليق مثبت LeakCanary غير متوفر - ضبط الصوت من خلال النغمات الموسيقية النصفية - خطوة الإيقاع الافتراضي ExoPlayer تغيير حجم الفاصل الزمني للتحميل (حاليا %s). قد تؤدي القيمة الأقل إلى تسريع تحميل الفيديو الأولي. تتطلب التغييرات إعادة تشغيل المشغل. تكوين إشعار مشغل البث الحالي @@ -763,4 +760,14 @@ %s دفق جديد %s دفق جديد + النسبة المئوية + سيميتون + لا يتم عرض التدفقات التي لم يدعمها برنامج التنزيل بعد + الدفق المحدد غير مدعوم من قبل المشغلون الخارجيون + لا توجد تدفقات صوتية متاحة للمشغلات الخارجية + لا تتوفر تدفقات فيديو للاعبين الخارجيين + حدد الجودة للمشغلين الخارجيين + تنسيق غير معروف + جودة غير معروفة + حجم الفاصل الزمني لتحميل التشغيل \ No newline at end of file diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 51683fb89..95b7b76f4 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -2,145 +2,145 @@ Başlamaq üçün \"Axtarış\" bölməsinə toxunun. %1$s tarixində yayımlanıb - Axın pleyeri tapılmadı. \"VLC\" yüklənilsin\? - Axın pleyeri tapılmadı (baxmaq üçün \"VLC\"ni yükləyə bilərsiniz). - Quraşdır + Yayım oynadıcı tapılmadı. \"VLC\" yüklənilsin\? + Yayım oynadıcı tapılmadı (baxmaq üçün \"VLC\"\'ni yükləyə bilərsiniz). + Yükləyin İmtina - Brauzerdə aç - Paylaş - Yüklə - Axın faylını endirin - Axtarış - Ayarlar + Brauzerdə açın + Paylaşın + Endirin + Yayım faylını endirin + Axtarın + Tənzimləmələr Bunu demək istədiniz: \"%1$s\"\? - Paylaş - Kənar video oynadıcı istifadə et - Bəzi görüntü keyfiyyətlərində səs itir - Kənar audio oynadıcı istifadə et - Abunə ol + ...ilə paylaşın + Xarici video oynadıcı istifadə edin + Bəzi keyfiyyət seçimlərində səsi silir + Xarici səs oynadıcı istifadə edin + Abunə Olun Abunə olundu Kanal abunəliyindən çıxıldı Məlumat göstər Abunəliklər - Saxlanmış Oxutma Siyahıları - Yeni nə var - Fon - Video yükləmə qovluğu - Yüklənmiş videolar burada saxlanılır - Video faylları üçün yükləmə qovluğunu seçin - Audio yükləmə qovluğu - Yüklənmiş audio faylları burada saxlanılır - Audio faylları üçün yükləmə qovluğunu seçin - Standard görüntü keyfiyyəti - Daha böyük ölçüləri göstər - \"Kodi\" ilə oxut + Əlfəcinlənmiş Pleylistlər + Yeniliklər + Arxa Fon + Video endirmə qovluğu + Endirilmiş video fayllar burada saxlanılır + Video faylları üçün endirmə qovluğunu seçin + Səs endirmə qovluğu + Endirilmiş səs faylları burada saxlanılır + Səs faylları üçün endirmə qovluğunu seçin + Defolt keyfiyyət + Daha böyük keyfiyyət seçimləri göstər + \"Kodi\" ilə Oynat Çatışmayan \"Kore\" tətbiqi yüklənilsin\? - \"Kodi ilə oxut\" seçimini göstər - Videonu Kodi media center ilə oynatmaq üçün seçim göstər - Audio - Standart səs formatı - Standart video formatı + \"Kodi ilə Oynat\" seçimini göstər + Videonu Kodi media mərkəzi ilə oynatmaq üçün seçim göstər + Səs + Defolt səs formatı + Defolt video formatı Mövzu - Açıq + İşıqlı Qaranlıq Qara - Abunəlikdən çıx - Ani pəncərə rejimində aç - Avto-oxutma - Yüklə - Kəsintilərdən sonra (məs. telefon zəngi) oxutmağa davam et - Oxutmağa davam et - İzlənmiş videoları qeyd et - Verilənləri təmizlə - Oxutma siyahılarındakı oxutma mövqeyi göstəricisini nümayiş et + Abunəlikdən çıxın + Ani pəncərə rejimində açın + Avto-oynatma + Endirin + Fasilələrdən sonra (məsələn, telefon zəngləri) oynatmağa davam etdirin + Oynatmanı davam etdir + Baxılmış videoların saxlanılması + Məlumat təmizləmə + Siyahılarda oynatma mövqelərini göstərin Siyahılardakı mövqelər - Video oxutmanı axırıncı qaldığı yerdən bərpa et - Oxutmanı davam etdir - İzləmə tarixçəsi - Axtarış sorğularını lokal olaraq saxlayın + Son oynatma mövqeyinə qaytarın + Oynatmanı davam etdir + Baxış tarixçəsi + Axtarış sorğularını yerli olaraq saxlayın Axtarış tarixçəsi Axtarış edərkən göstəriləcək təklifləri seçin Axtarış təklifləri - Pleyerin parlaqlığını idarə etmək üçün jestlərdən istifadə edin - Parlaqlığın jestlə idarə edilməsi - Pleyerin səsini idarə etmək üçün jestlərdən istifadə edin - Səsin jestlə idarə edilməsi + Oynadıcının parlaqlığını nizamlamaq üçün jestlərdən istifadə edin + Parlaqlığı jestlə nizamlama + Oynadıcının səsini nizamlamaq üçün jestlərdən istifadə edin + Səsi jestlə nizamlama Avto-növbələmə - Növbəti axını avtomatik olaraq növbəyə əlavə et - Metadata keşi silindi - Keşlənmiş bütün veb-səhifə verilənlərini sil - Keşlənmiş metadatanı təmizlə + Növbəti Yayımı Avto-növbələmə + Üst məlumat keşi silindi + Keşlənmiş bütün veb-səhifə məlumatlarını silin + Keşlənmiş üst məlumatı təmizləyin Şəkil keşi silindi Şərhləri gizlətmək üçün söndürün Şərhləri göstər - Aktiv pleyerin növbəsi dəyişdiriləcək - Bir pleyerdən digərinə keçmək növbənizi dəyişdirə bilər - Növbəni təmizləmədən öncə təsdiq üçün soruş - Qeyri-dəqiq axtarış (videonu irəli/geri çəkmə) istifadə edin - Qeyri-dəqiq axtarış oynadıcıya azaldılmış həssaslıqla mövqeləri daha sürətlə axtarmağa imkan verir. 5, 15 və ya 25 saniyəlik axtarış bununla işləmir - Cəld irəli/geri çəkmə müddəti + Aktiv oynadıcının növbəsi dəyişdiriləcək + Bir oynadıcıdan digərinə keçid növbənizi dəyişdirə bilər + Növbəni təmizləməzdən əvvəl təsdiq üçün soruş + Sürətli qeyri-dəqiq axtarışdan istifadə edin + Qeyri-dəqiq axtarış oynadıcıya azaldılmış dəqiqliklə mövqeləri daha sürətli axtarmağa imkan verir. 5, 15 və ya 25 saniyəlik axtarış bununla işləmir + Sürətli irəli/geri çəkmə axtarış müddəti Heç nə - Buferizasiya olunur + Buferlənir Qarışdır Təkrarla - Beşinci hərəkət düyməsi - Dördüncü hərəkət düyməsi - Üçüncü hərəkət düyməsi - İkinci hərəkət düyməsi - Birinci hərəkət düyməsi - Yalnız bəzi cihazlar 2K/4K videoları oxuda bilir - Ani pəncərədə standart görüntü keyfiyyəti - Əlavə et - Ani pəncərə (popup) - Tab-vərəqəni Seçin - Abunəlik yenilənmədi - Abunəlik dəyişdirilmədi + Beşinci fəaliyyət düyməsi + Dördüncü fəaliyyət düyməsi + Üçüncü fəaliyyət düyməsi + İkinci fəaliyyət düyməsi + Birinci fəaliyyət düyməsi + Yalnız bəzi cihazlar 2K/4K videoları oynada bilir + Defolt ani pəncərə keyfiyyəti + Əlavə Edin + Ani Pəncərə + Tabı Seçin + Abunəliyi yeniləmək alınmadı + Abunəliyi dəyişdirmək alınmadı Nəticələr göstərilir: %s Kanallar %s tərəfindən - \"Youtube\"un \"Məhdudiyyətli Rejimi\"ni aktivləşdir - Yaş limiti olduğuna görə (məs. 18+) böyük ehtimal uşaqlar üçün uyğun olmayan məzmunu göstər + YouTube\'un \"Məhdud Rejimi\"ni açın + Yaş həddi səbəbiylə (məsələn, 18+) uşaqlar üçün uyğun olmayan məzmunu göstərin Yaş məhdudiyyətli məzmunu göstər Məzmun - Ani pəncərədə oxudulur - Fonda oxudulur + Ani pəncərədə oynadılır + Arxa fonda oynadılır Yeniləmələr Sazlama Görünüş - Tarix və keş + Tarixçə və keş Video və səs Davranış - Pleyer - İlkin məzmun dili - Məzmun üçün ilkin ölkə - URL tanınmadı. Başqa bir tətbiq ilə açılsın\? - Dəstəklənməyən URL - \"Əlavə etmək üçün basılı tutun\" məsləhətini göstər + Oynadıcı + Defolt məzmun dili + Defolt məzmun ölkəsi + URL\'i tanımaq olmadı. Başqa tətbiqlə açılsın\? + Dəstəklənməyən URL\'i + \"Növbəyə əlavə etmək üçün basılı saxla\" ipucusun göstər \"Növbəti\" və \"Bənzər\" videoları göstər - Tarixçəni, abunəlikləri və oxutma siyahılarını ixrac et - "Cari tarixçə, abunəliklər, pleylistlər və (istəyə görə) ayarlarınızın üzərinə yazır" - reCAPTCHA çərəzləri təmizləndi - reCAPTCHA çərəzlərini təmizlə - Məlumat bazasını ixrac et - Məlumat bazasını idxal et - Əsas Görünüşə Keç - Ani Pəncərəyə Keç - Fona Keç - [Bilinməyən] + Tarixçəni, abunəlikləri, pleylistləri və tənzimləmələri ixrac edin + Cari tarixçənizi, abunəliklərinizi, pleylistlərinizi və (istəyə görə) tənzimləmələrinizi etibarsız edir + reCAPTCHA kukiləri təmizləndi + reCAPTCHA kukilərini təmizləyin + Məlumat bazasını ixrac edin + Məlumat bazasını idxal edin + Əsas Görünüşə Keçid + Ani Pəncərəyə Keçid + Arxa Fona Keçid + [Naməlum] Yeni \"NewPipe\" versiyası üçün bildirişlər - Tətbiq Yeniləmə Bildirişi - Fon və ani pəncərə pleyerləri üçün \"NewPipe\" bildirişləri + Tətbiq yeniləmə bildirişi + NewPipe oynadıcısı üçün bildirişlər Hamısı Xəta hesabatı - Yükləmələr - Yükləmələr + Endirmələr + Endirmələr Canlı Bu video yaş məhdudiyyətlidir. \n -\nGörmək istəyirsinizsə, ayarlarda \"%1$s\" özəlliyini yandırın. - \"YouTube\" böyüklər üçün potensial məzmunu gizləyən \"Məhdud Rejim\" təmin edir. - \"PeerTube\" nümunələri - Kiçik təsvirləri yüklə +\nOnu görmək istəyirsinizsə, tənzimləmələrdə \"%1$s\" seçimini aktivləşdirin.
+ \"YouTube\" böyüklər üçün potensial məzmunu gizləyən \"Məhdud Rejim\" təmin edir + \"PeerTube\" serverləri + Miniatürləri yükləyin Siz yığcam bildirişdə göstərilməsi üçün ən çoxu üç fəaliyyət seçə bilərsiniz! Həmişə yenilə Axın @@ -152,75 +152,75 @@ %d seçildi %d seçildi - Abunəlik seçilmədi + Abunəlik seçilməyib Abunəlikləri seçin Axın emal edilir… Axın yüklənir… Yüklənmədi: %d - Oxutma siyahısı səhifəsi - \"Newpipe\" Bildirişi + Pleylist səhifəsi + \"Newpipe\" bildirişi Fayl Yalnız Bir Dəfə Həmişə - Hamısını Oxut + Hamısını Oynat Fayl silindi Geri qaytar - Ən yaxşı görüntü keyfiyyəti - Təmizlə - Deaktiv edilib - İfaçılar + Ən yaxşı keyfiyyət + Təmizləyin + Qeyri-aktivdir + Rəssamlar Albomlar Mahnılar Hadisələr İstifadəçilər Treklər Videolar - Oxutma siyahıları + Pleylistlər Xəta Kömək - Axtarış tarixçəsi silindi. + Axtarış tarixçəsi silindi Bütün axtarış tarixçəsi silinsin\? - Axtarışda işlədilmiş açar sözlərin tarixçəsini siləcək - Axtarış tarixçəsini təmizlə - Oxutma mövqeləri silindi. - Bütün oxutma mövqeləri silinsin\? - Bütün oxutma mövqelərini siləcək - Oxutma mövqelərini sil - Baxış tarixçəsi silindi. + Axtarışdakı açar sözlərin tarixçəsini silir + Axtarış tarixçəsini silin + Oynatma mövqeləri silindi + Bütün oynatma mövqeləri silinsin\? + Bütün oynatma mövqelərini siləcək + Oynatma mövqelərini silin + Baxış tarixçəsi silindi Bütün baxış tarixçəsi silinsin\? Baxış tarixçəsini təmizlə - \"ReCAPTCHA\" həll edilərkən \"NewPipe\"ın saxladığı çərəzləri təmizlə + reCAPTCHA həll edərkən NewPipe\'ın saxladığı kukiləri silin %s tərəfindən yaradıldı Yaxınlaşdır Doldur Dart - Altyazı yoxdur - Sil - Hələ ki kanal abunəliyi yoxdur + Altyazı Yoxdur + Silin + Hələ ki, kanal abunəliyi yoxdur Kanal seçin Kanal Səhifəsi İlkin Köşk Köşk Səhifəsi Boş Səhifə - Ana səhifədə hansı tab-vərəqələr göstərilir + Ana səhifədə hansı tablar göstərilir Ana səhifənin məzmunu - Yeni versiya mövcud olanda tətbiqi yeniləməyi xatırlatmaq üçün bildiriş göstər + Yeni versiya mövcud olduqda tətbiq yeniləməsini xatırlatmaq üçün bildiriş göstər Yeniləmələr Mobil internet istifadə edərkən görüntü keyfiyyətini məhdudlaşdırın Limitsiz 1 element silindi. - Nümunə əlavə et - Sevimli \"PeerTube\" nümunələrinizi seçin - Yüklənmiş faylları sil - Yükləmə tarixçənizi təmizləmək və ya yüklənmiş bütün faylları silmək istəyirsiniz\? - Yükləmə tarixçəsini təmizlə - Yükləmələrə başla - Yükləmələrə fasilə ver - Haraya yüklənəcəyini soruş - Hər yükləmədə harada saxlanılacağı soruşulacaq - \'Saxlanca Müraciət Çərçivəsi\' xarici SD karta yükləməyə imkan verir. -\nBəzi cihazlar uyğun deyil - Sistem öncülü + Server əlavə edin + Sevimli \"PeerTube\" serverlərinizi seçin + Endirilmiş faylları silin + Endirmə tarixçənizi təmizləmək və ya endirilmiş bütün faylları silmək istəyirsiniz\? + Endirmə tarixçəsini təmizlə + Endirmələrə başla + Endirmələrə fasilə verin + Haraya endiriləcəyini soruş + Sizdən hər endirmənin harada saxlanacağı soruşulacaq. +\nXarici SD karta yükləmək istəyirsinizsə, sistem qovluğu seçicisini (SAF) aktiv edin + \'Yaddaş Giriş Çərçivəsi \' xarici SD karta endirməyə imkan verir + Sistem defoltu Tətbiq dili %d gün @@ -238,11 +238,11 @@ %d saniyə %d saniyə - Axın sonuncu dəfə güncəlləndi: %s - Axın güncəlləmə astanası - Sürətli rejimi aktiv et + Axın sonuncu dəfə yeniləndi: %s + Axın yeniləmə astanası + Sürətli rejimi aktivləşdir Sürətli rejimi deaktiv et - Axının çox yavaş yükləndiyini düşünürsünüz\? Əgər elədirsə, sürətli yükləməni işə salmağı sınayın (ayarlardan dəyişə və ya aşağıdakı düyməni basa bilərsiniz). + Axının çox yavaş yükləndiyini düşünürsünüz\? Əgər elədirsə, sürətli yükləməni işə salmağı sınayın (tənzimləmələrdən dəyişə və ya aşağıdakı düyməni basa bilərsiniz). \n \nNewPipe axını yükləmək üçün 2 metod təklif edir: \n• Bütün abunəlik kanallarını gətirtmək, bu yavaş olsa da tamdır; @@ -250,46 +250,472 @@ \n \nBu ikisi arasında fərq odur ki, sürətlisində, adətən elementin müddəti və növü kimi bəzi məlumatlar çatışmır (canlı video ilə adisini ayırd edə bilmir) və daha az element gətirir. \n -\nYouTube öz RSS axını ilə bu sürətli metodu təklif edən xidmətlərdən biridir. +\nYouTube öz RSS axını ilə bu sürətli metodu təklif edən xidmətlərdən biridir. \n \nBeləliklə, seçim sizin nəyə üstünlük verməyinizdən asılıdır: sürət yoxsa dəqiq məlumat. - Bu axını oxutmaq olmadı + Bu yayımı oynatmaq alınmadı Tətbiq/UI çökdü - Yükləmə menyusu qurulmadı + Endirmə menyusunu qurmaq mümkün olmadı Məzmun əlçatmazdır - Kiçik təsvirlərin hamısı yüklənmədi + Bütün miniatürləri yükləmək alınmadı Şəbəkə xətası - Xarici SD karta yükləmək mümkün deyil. Yükləmə qovluğu üçün təyin edilmiş yer sıfırlansın\? - Xarici saxlanc əlçatmazdır - Oxutma axının tarixçəsini və oxutma mövqelərini siləcək - Üst bilgini göstər - Video təsvirini və əlavə bilgiləri gizlətmək üçün söndürün - Təsviri göstər - Bildirişləri rənglə + Xarici SD karta endirmək mümkün deyil. Endirmə qovluğunun yeri sıfırlansın\? + Xarici yaddaş əlçatan deyil + Oynadılmış yayımların tarixçəsini və oynatma mövqelərini silir + Üst məlumatı göstər + Video açıqlamasını və əlavə məlumatı gizlətmək üçün söndürün + Açıqlamanı göstər + Bildirişi rəngləyin Belə qovluq yoxdur - Əsas oynadıcını (pleyeri) tam ekranda başlat + Əsas oynadıcını tam ekranda başlat Xarici oynadıcılar bu cür linkləri dəstəkləmir Yerli axtarış təklifləri Video - Əlaqəli videolar - İzlənmiş kimi işarələ - Aşağıdakılardan biri ilə aç + Əlaqədar yayımlar + Baxılmış kimi işarələ + ...ilə açın Gecə Mövzusu - Ani açılan pəncərə (popup) xüsusiyyətlərini xatırla - Ani açılan pəncərənin son ölçüsü və mövqeyini xatırla + Ani pəncərə xüsusiyyətlərini xatırla + Ani pəncərənin son ölçüsü və mövqeyini xatırla Video yayımı tapılmadı Şərhlər - Təsvir - + Açıqlama + Burada kriketlərdən başqa heç nə yoxdur Nəticə yoxdur - İlkin ayarları qaytar + İlkin tənzimləmələri qaytarın Fayl köçürüldü və ya silindi - Oynadıcı xətası düzəldilir - Bərpa oluna bilməyən oynadıcı xətası baş verdi + Oynadıcı xətası bərpa edilir + Bərpa olunmayan oynatma xətası baş verdi Oldu - Bu video yaş məhdudiyyətlidir. -\n\"YouTube\"un yeni yaş məhdudiyyətli videolar siyasətinə görə \"NewPipe\" bu cür videoların yayımını əldə edə bilmir, beləliklə, videonu oynatmaq mümkün deyil. + Bu video yaş məhdudiyyətidir. +\nYaş məhdudiyyəti olan videolarla bağlı yeni YouTube siyasətlərinə görə, NewPipe bu cür video yayımlara daxil ola və oynada bilməz. Səs yayımı tapılmadı - Başqa proqramların üzərində göstərmə icazəsi ver - İlkin ayarları qaytarmaq istəyirsiniz\? + Digər tətbiqlərin üzərində göstərməyə icazə verin + İlkin tənzimləmələri qaytarmaq istəyirsiniz\? + Miniatürlərin yüklənməsini, dataya qənaət etmək və yaddaşdan istifadəni azaltmaq üçün söndürün. Dəyişikliklər həm yaddaşdaxili, həm də diskdə olan təsvir keşini təmizləyir + Növbətini sıraya salın + Yenidən Cəhd Edin + Cari oynatma yayımı bildirişini konfiqurasiya edin + Bildirişlər + Video fayl xülasəsi bildirişi + Abunəliklər üçün yeni yayımlar haqqında bildirişlər + Xəta hesabatları üçün bildirişlər + Fayl adı boş ola bilməz + Yadda saxlanmış tabları oxumaq mümkün olmadı, buna görə defolt tablardan istifadə edin + NewPipe xəta ilə qarşılaşdı, bildirmək üçün toxunun + Bağışlayın, belə olmamalı idi. + Bu xətanı e-poçt vasitəsilə bildirin + GitHub\'da Hesabat Verin + Zəhmət olmasa, xətanızı müzakirə edən məsələnin mövcud olub-olmadığını yoxlayın. Dublikat biletləri yaradarkən, bizdən faktiki səhvi düzəltməyə sərf edəcəyimiz vaxt alırsınız. + Hesabat Bildirin + Məlumat: + Nə baş verdi: + Yükləyənin avatar miniatürü + Bəyəni + Bəyənməmə + Yenidən sıralamaq üçün sürüşdürün + m + M + M + Xidməti dəyişin,hazırda seçilmiş: + Abunəçi yoxdur + Baxış yoxdur + Heç kim izləmir + Heç kim dinləmir + Video yoxdur + Şərhlər qeyri-aktivdir + Başladın + Dayandırın + Təsdiqləmə + İmtina + Xəta + Detallar üçün toxunun + Zəhmət olmasa, gözləyin… + Hələ endirmə qovluğu təyin edilməyib, indi defolt endirmə qovluğunu seçin + reCAPTCHA çağırışı + reCAPTCHA sorğusu göndərildi + Bitdi + Etibarsız simvollar bu dəyərlə əvəz olunur + Əvəzedici xarakter + Ən xüsusi simvollar + Üçüncü Tərəf Lisenziyaları + Haqqında + Töhfə Verin + Fikirlərinizin olub-olmaması, tərcümə, dizayn dəyişiklikləri, kodun təmizlənməsi və ya real ağırlıqlı kod dəyişiklikləri və.s kömək həmişə xoşdur. Nə qədər çox edilsə, bir o qədər yaxşı olar! + İanə Edin + Veb sayt + Əlavə məlumat və xəbərlər üçün NewPipe Veb saytına daxil olun. + NewPipe\'ın Məxfilik Siyasəti + NewPipe layihəsi məxfiliyinizə çox ciddi yanaşır. Buna görə də, tətbiq sizin razılığınız olmadan heç bir məlumat toplamır. +\nNewPipe\'ın məxfilik siyasəti qəza hesabatı göndərdiyiniz zaman hansı məlumatların göndərildiyini və saxlandığını ətraflı izah edir. + Məxfilik siyasətini oxuyun + NewPipe\'ın Lisenziyası + Tarixçə + Bu elementi axtarış tarixçəsindən silmək istəyirsiniz\? + Son Oynadılan + Ən çox oynadılan + Köşk seçin + İdxal edildi + Etibarlı ZIP faylı yoxdur + Xəbərdarlıq: Bütün faylları idxal etmək mümkün olmadı. + Tənzimləmələri də idxal etmək istəyirsiniz\? + Tətbiq yenidən başladıldıqdan sonra dil dəyişəcəkdir + Ən yaxşı 50 + Yeni və populyar + Yerli + Son əlavə edilən + Konfranslar + Oynatma növbəsi + Detallar + Kanal təfərrüatlarını göstərin + Ani pəncərədə oynatmağa başlayın + \"Açıq\" fəaliyyətə üstünlük verilir + Arxa Fon oynadıcı + Həmişə soruşun + Tələb olunan məzmun yüklənir + Yeni Pleylist + Adını dəyişdirin + Pleylistə əlavə edin + Emal edilir... Bir az vaxt ala bilər + Səsi aç + Pleylisti Əlfəcinlə + Əlfəcini Silin + Bu pleylist silinsin\? + Pleylist yaradıldı + Pleylist miniatürü dəyişdirildi. + Avtomatik yaradıldı (heç bir yükləyici tapılmadı) + Avtomatik yaradıldı + Altyazılar + LeakCanary yoxdur + Yaddaş sızmasının monitorinqi yığın boşaltma zamanı tətbiqin cavab verməməsinə səbəb ola bilər + Yaddaş sızmalarını göstərin + Utilizasiyadan sonra fraqment və ya fəaliyyətin yaşam dövründən kənarda çatdırıla bilməyən Rx istisnaları barədə hesabat verməyə məcbur edin + Xidmətlərdən alınmış orijinal mətnlər yayım elementlərində görünəcək + Yeni yayımları yoxlayın + URL və ya ID\'nizi daxil etməklə SoundCloud profilini idxal edin: +\n +\n1. Veb-brauzerdə \"iş masası rejimini\" aktiv edin (sayt mobil cihazlar üçün mövcud deyil) +\n2. Bu URL\'ə keçin: %1$s +\n3. Soruşulduqda daxil olun +\n4. Yönləndirildiyiniz profilin URL\'sini kopyalayın. + ID\'niz, soundcloud.com/ID\'niz + Unutmayın ki, bu əməliyyat şəbəkəyə ağır yük ola bilər. +\n +\nDavam etmək istəyirsiniz\? + Sükut zamanı sürətlə irəlilə + Yeni yayım bildirişləri + Abunəliklərdən yeni yayımlar haqqında bildiriş göndər + Tezliyin yoxlanılması + Tələb olunan şəbəkə bağlantısı + İstənilən şəbəkə + Tətbiq keçidində kiçildin + Arxa Fon oynadıcısına kiçildin + Ani-pəncərə oynadıcısına kiçildin + Oynatmağa avtomatik başlayın — %s + Aşağı keyfiyyətli (daha kiçik) + Göstərməyin + Endirmə uğursuz oldu + Server çox iş parçalı endirmələri qəbul etmir, @string/msg_threads = 1 ilə yenidən cəhd edin + Bütün endirilmiş fayllar diskdən silinsin\? + Maksimum təkrar cəhdlər + Pleylistə əlavə olunandan əvvəl və sonra baxılmış videolar silinəcək. +\nSiz əminsiniz\? Bu geri qaytarıla bilməz! + Kanal qrupları + Yeni axın elementləri + Abunəlik köhnəlmiş hesab edilənə qədərki son yeniləmədən sonrakı vaxt — %s + Axın yükləmə xətası + Bu məzmun hələ NewPipe tərəfindən dəstəklənmir. +\n +\nÜmid edirik ki, gələcək versiyada dəstəklənəcək. + Həm kilid ekranı fonu, həm də bildirişlər üçün miniatürdən istifadə edin + Ən Yeni + Bu məzmun ölkənizdə mövcud deyil. + Bu məzmun yalnız ödəniş etmiş istifadəçilər üçün əlçatandır, ona görə də NewPipe tərəfindən yayımlana və ya endirilə bilməz. + Avtomatik (cihaz mövzusu) + Sevimli gecə mövzusunu seçin — %s + Sabitlənmiş şərh + Bildirişlər deaktiv edilib + Bildiriş alın + Artıq bu kanala abunə oldunuz + , + Hamısını dəyişdirin + Fayl adı + Həll edin + Abunəlikləri ixrac etmək mümkün olmadı + + %s izləyici + %s izləyici + + Yeni versiyaları əlinizlə yoxlayın + + %s dinləyici + %s dinləyici + + + %s video + %s video + + Yeniləmələri yoxlayın + Axtarış çubuğunun miniatür önizləməsi + Əməliyyat sistem tərəfindən ləğv edildi + Avto + Tapılmadı + Server məlumat göndərmir + Bu endirməni bərpa etmək mümkün deyil + Sizdən hər endirmənin harada saxlanacağı soruşulacaq + \'Yaddaş Giriş Çərçivəsi\' Android KitKat və ondan aşağı versiyalarda dəstəklənmir + \"Yaddaş Giriş Çərçivəsi\"yalnız Android 10\'dan başlayaraq dəstəklənir + Kanalın avatar miniatürü + Sevdiyiniz gecə mövzusunu aşağıda seçə bilərsiniz + Android\'in bildiriş rəngini miniatürdəki əsas rəngə uyğun fərdiləşdirməsini təmin edin(qeyd edək ki, bu, bütün cihazlarda mövcud deyil) + GitHub\'da Baxın + İanə Edin + NewPipe, sizə ən yaxşı istifadəçi təcrübəsini göstərmək üçün boş vaxtlarını sərf edən könüllülər tərəfindən hazırlanmışdır. Tərtibatçılara bir fincan qəhvə içərkən NewPipe\'ı daha da yaxşılaşdırmağa kömək etmək üçün kömək edin. + Ən çox bəyənildi + Növbəyə salındı + Məzmunu açarkən defolt hərəkət — %s + Ad + Pleylist miniatürü kimi təyin edin + Yalnız Wi-Fi\'da + Heç vaxt + Siyahı görünüş rejimi + Siyahı + Tor + Yüksək keyfiyyətli (daha böyük) + Gözlənilir + növbədə + son proseslər tətbiq olunur + Yeniləmələr yoxlanılır… + NewPipe yeniləməsi mövcuddur! + Lisenziya + Müəllifin hesabı bağlanıb. +\nNewPipe gələcəkdə bu axını yükləyə bilməyəcək. +\nBu kanala abunəlikdən çıxmaq istəyirsiniz\? + Baxılmış elementləri göstərin + Seçilmiş + Çəkməcəni Bağlayın + Video oynadıcı + Video fayl xülasəsi prosesi üçün bildirişlər + Açın + Kiçik şəkili 1:1 aspekt nisbətinə ölçün + Yükləmə intervalının həcmini dəyişdirin (hazırda %s). Daha aşağı dəyər ilkin video yükləməni sürətləndirə bilər. Dəyişikliklər oynadıcının yenidən başladılmasını tələb edir. + Yayım yaradıcısı, məzmunu və ya axtarış sorğusu haqqında əlavə məlumat olan üst məlumat qutularını gizlətmək üçün söndürün + Əlaqədar yayımı əlavə etməklə (təkrar etməyən) sonlanacaq oynatma sırasını davam etdir + Kənar axtarış təklifləri + Server artıq mövcuddur + Videoları mini oynadıcıda başlatma, avtomatik fırlatma kilidlidirsə, birbaşa tam ekran rejiminə keçid. Siz hələ də tam ekran rejimindən çıxmaqla mini pleyerə daxil ola bilərsiniz + 100+ video + ∞ video + Şərhlər yoxdur + Həll edildikdə \"Bitdi\" düyməsini basın + Pleylist seçin + Şərhləri yükləmək mümkün olmadı + Populyar + Səs Tənzimləmələri + Məlumat əldə edilir… + Elementlərdə əvvəlki vaxtı göstərin + Yaşam dövrəsi xaricindəki xətaları bildirin + Şəkil göstəricilərini göstərin + Şəkillərin üzərində mənbəsini göstərən Pikasso rəngli lentləri göstərin: şəbəkə üçün qırmızı, disk üçün mavi və yaddaş üçün yaşıl + Bəzi endirmələri dayandırmaq mümkün olmasa da, mobil dataya keçərkən faydalıdır + Bağla + Fayl silindiyi üçün irəliləyiş itirildi + Endirməni ləğv etməzdən əvvəl ümumi cəhdlərin sayı + Ölçülmüş şəbəkələrdə dayandır + Endirmə növbəsini məhdudlaşdırın + Eyni vaxtda ancaq bir endirmə həyata keçiriləcək + Hesab ləğv edildi + %s bu səbəbi təmin edir: + Endirmə başladı + Açıqlamadakı mətni seçməyi deaktiv edin + Kateqoriya + Daxili + Açıqlamadakı mətni seçməyi aktivləşdirin + Teqlər + Planşet rejimi + Bağlayın + Yaradıcısından ürəkləndi + Veb saytı açın + + %s baxış + %s baxış + + Addım + + %s yeni yayım + %s yeni yayım + + Sıfırlayın + Faiz + Yarımton + + Endirmə tamamlandı + %s endirmələr tamamlandı + + Defolt ExoPlayer + Mövcud olduqda xüsusi axından alın + Baxılmış videolar silinsin\? + İzlənilmişi silin + Sistem qovluğu seçicisini (SAF) istifadə edin + Bağlantı fasiləsi + Cihazda yer qalmayıb + Fayl üzərində işləyərkən NewPipe bağlandı + Emaldan sonra uğursuz oldu + Serverə qoşulmaq mümkün deyil + Serveri tapmaq olmadı + Təhlükəsiz əlaqə qurmaq olmadı + Fayl yaradıla bilməz + Bu adla bir endirmə davam edir + faylın üzərinə yazıla bilməz + Bu adda endirilmiş fayl artıq mövcuddur + Üzərinə yazın + Növbəyə qoyun + bərpa olunur + dayandırıldı + Bitdi + Endirmək üçün toxunun + Heç biri + Əsas video oynadıcıdan digər tətbiqə keçid zamanı hərəkət — %s + İmtina + Razıyam + Sürət + İdxal + Əvvəlki ixrac + İxrac edilir… + İdxal edilir… + Pleylistə salındı + Səsi bağla + Ani pəncərə oynadıcı + Çəkməcəni Açın + Növbəyə saxlamaq üçün basılı tutun + Silin + Android\'də pulsuz yüngül yayımlayıcı. + © %1$s, %2$s tərəfindən %3$s altında + Bu faylı oynatmaq üçün heç bir tətbiq quraşdırılmayıb + Endirmə + Bu icazə, ani pəncərə rejimində +\naçmaq üçün lazımdır + Buferə kopyalandı + Parçalar + Adını dəyişdir + Yaradın + + %s abunəçi + %s abunəçi + + Səs + Təfərrüatlar: + Nə:\\nTələb:\\nMəzmun Dili:\\nMəzmun Ölkəsi:\\nTətbiq Dili:\\nXidmət:\\nGMT Saatı:\\nPaket:\\nVersiya:\\nƏS versiyası: + Bağışlayın, nəsə xəta baş verdi. + Formatlanmış hesabatı kopyalayın + Server URL\'sini daxil edin + Serveri doğrulamaq mümkün olmadı + %s-də bəyəndiyiniz serverləri tapın + Video \"Təfsilatlar:\"səhifəsində arxa fon və ya ani pəncərə düyməsini basarkən ipucu göstər + Oynadıcı altyazı miqyasını və arxa fon üslublarını dəyişdirin. Effektiv olması üçün tətbiqin yenidən başladılması tələb olunur + Xəta baş verdi: %1$s + Fayl mövcud deyil, yaxud oxumaq və ya yazmaq icazəsi yoxdur + Veb saytı təhlil etmək alınmadı + Səs ucalığı + Radio + \"Oynadıcını çökdür\" Göstərin + Oynadıcıdan istifadə edərkən çökdürmə seçimini göstərin + Xəta balonu göstər + Xəta bildirişi yaradın + Burdan idxal edin + Bura ixrac edin + Faylı idxal edin + Abunəlikləri idxal etmək mümkün olmadı + Avropa Ümumi Məlumat Mühafizəsi Qaydasına (GDPR) riayət etmək üçün diqqətinizi NewPipe\'ın məxfilik siyasətinə cəlb edirik. Zəhmət olmasa, diqqətlə oxuyun. Xəta hesabatını bizə göndərmək üçün onu qəbul etməlisiniz. + Bu adda fayl artıq mövcuddur + Bu adla gözlənilən bir endirmə var + Təyinat qovluğu yaradıla bilməz + Bənzərsiz ad yaradın + Bölmələr + Cihazınızdakı heç bir tətbiq bunu aça bilməz + Miniatürü göstərin + Bu, ən azı sizin ölkənizdə olan SoundCloud Go+ trekidir, ona görə də NewPipe tərəfindən yayımlamaq və ya endirmək mümkün deyil. + Bu məzmun şəxsidir, ona görə də NewPipe tərəfindən yayımlamaq və ya endirmək mümkün deyil. + Dəstək + Məxfilik + Sahib + Siyahıdan kənar + Şəxsi + Miniatür URL + Yaş həddi + Dil + İctimai + Abunəçi sayı əlçatan deyil + Lisenziyanı oxuyun + Tarixçə + Hərflər və rəqəmlər + Oynadıcını çökdür + Yalnız HTTPS URL\'ləri dəstəklənir + Oynadıcı bildirişi + Yeni yayımlar + Xəta hesabatı bildirişi + Video URL\'i imzasının şifrəsi qırılmadı + Endirmək üçün heç bir yayım yoxdur + Xəta baş verdi, bildirişə baxın + Şərhiniz (İngilis dilində): + Videonu oynadın, müddət: + Zəhmət olmasa, daha sonra tənzimləmələrdə endirmə qovluğunu təyin edin + NewPipe Endirilir + Hash hesablanır + Fayl adlarında icazə verilən simvollar + NewPipe Haqqında + Lisenziyalar + NewPipe müəllif hüquqlu sərbəst tətbiqdir: Siz onu istədiyiniz zaman istifadə edə, öyrənə, paylaşa və təkmilləşdirə bilərsiniz. Xüsusilə, siz Lisenziyanın 3-cü versiyası və ya (sizin seçiminizə görə) hər hansı sonrakı versiyada Azad Proqram Təminatı Fondu tərəfindən dərc edilən GNU Ümumi İctimai Lisenziyasının şərtlərinə uyğun olaraq onu yenidən paylaya və/yaxud dəyişdirə bilərsiniz. + İxrac edildi + Elementləri silmək üçün sürüşdürün + Hələ,əlfəcinlənmiş pleylistlər yoxdur + Bu, cari quraşdırmanızı ləğv edəcək. + Növbəyə qoy + Qara ekranla qarşılaşsanız və ya videonu oynatdıqda səs qırılarsa, media tunelini deaktiv edin + Növbəti sıraya salındı + Arxa fonda oynatmağa başlayın + Yayım təfərrüatları yüklənir… + Media tunelini deaktiv edin + Tətbiq çökdü + YouTube abunəliklərini Google takeout\'dan +\nidxal edin: +\n +\n1. Bu URL\'ə keçin: %1$s +\n2. Soruşulduqda daxil olun +\n3.\"Bütün Məlumatlar Daxildir\",sonra \"Heçbirini Seçmə\", yalnız \"abunəliklər\"i seçin və \"OK\" kliklə +\n4. \"Növbəti addım\"üzərinə klikləyin, sonra isə \"İxrac Yarat\" üzərinə klikləyin +\n5. Görünəndən sonra \"Endirin\"düyməsini basın +\n6. Aşağıdakı FAYLI İDXAL ET düyməsinə klikləyin və endirilmiş .zip faylını seçin +\n7. [Əgər .zip faylı idxalı uğursuz olsa] .csv faylını çıxarın(adətən\"YouTubevəYouTubeMusic/subscriptions/subscriptions.csv\" altında),aşağıda İDXAL EDİLƏN FAYL-ı klikləyin və çıxarılmış csv faylını seçin + Oynatma Sürəti Nizamlamaları + Ayır (pozuntuya səbəb ola bilər) + Xətanı göstər + Bəli və qismən baxılmış videolar + + %1$s endirməsi silindi + %1$s endirmə silindi + + Dayandır + Nümunə seçin + Sürətli axın rejimi bu barədə əlavə məlumat vermir. + ExoPlayer məhdudiyyətlərinə görə axtarış müddəti %d saniyəyə təyin edildi + Bəzi xidmətlərdə mövcuddur, adətən daha sürətli olur, lakin məhdud sayda elementləri və çox vaxt natamam məlumatı qaytara bilər (məsələn, müddət, element növü, canlı status yoxdur) + Bu əməliyyat üçün uyğun fayl meneceri tapılmadı. +\nZəhmət olmasa, fayl menecerini quraşdırın və ya endirmə tənzimləmələrində \'%s\'-i deaktiv etməyə çalışın. + \'%s\' üçün axın yükləmək mümkün olmadı. + Bu əməliyyat üçün uyğun fayl meneceri tapılmadı. +\nZəhmət olmasa ,Yaddaş Giriş Çərçivəsinə uyğun fayl menecerini quraşdırın. + Bu video yalnız YouTube Music Premium üzvləri üçün əlçatandır, ona görə də NewPipe tərəfindən yayımlamaq və ya endirmək mümkün deyil. + İndi açıqlamadakı mətni seçə bilərsiniz. Nəzərə alın ki, seçim rejimində səhifə titrəyə bilər və keçidlər kliklənməyə bilər. + Bildirişdə göstərilən video miniatürünü 16:9-dan 1:1 nisbətinə qədər ölçün (pozuntulara səbəb ola bilər) + Aşağıdakı bildiriş fəaliyyətini hər birinin üzərinə toxunaraq redaktə edin. Sağdakı təsdiq qutularından istifadə edərək yığcam bildirişdə göstərilməsi üçün onlardan üçə qədərini seçin + Belə fayl/məzmun mənbəyi yoxdur + Seçilmiş yayım xarici oynadıcılar tərəfindən dəstəklənmir + Yükləyici tərəfindən hələ dəstəklənməyən yayımlar göstərilmir + Xarici oynadıcılar üçün heç bir səs yayımı yoxdur + Xarici oynadıcılar üçün heç bir video yayımı yoxdur + Xarici oynadıcılar üçün keyfiyyət seçin + Naməlum format + Naməlum keyfiyyət + Oynatma yükləmə intervalı həcmi \ No newline at end of file diff --git a/app/src/main/res/values-b+zh+HANS+CN/strings.xml b/app/src/main/res/values-b+zh+HANS+CN/strings.xml index c5b9e9646..b3642829c 100644 --- a/app/src/main/res/values-b+zh+HANS+CN/strings.xml +++ b/app/src/main/res/values-b+zh+HANS+CN/strings.xml @@ -670,12 +670,9 @@ 找不到适合此操作的文件管理器。 \n请安装与存储访问框架(SAF)兼容的文件管理器。 NewPipe 遇到了一个错误,点击此处报告此错误 - 已经在后台播放 置顶评论 LeakCanary 不可用 - 以音乐半音调整音高 - 节奏步长 - 改变加载间隔的大小(当前%s),较低的值可以加快初始的视频加载速度,改变需要重启播放器。 + 更改加载间隔的大小(当前为 %s),较低的值可以加快视频的首次加载速度。更改需要重启播放器。 ExoPlayer 默认 配置当前正在播放的串流的通知 新串流通知 @@ -699,4 +696,13 @@ 清除所有下载的文件? 获取通知 来自订阅的新串流的通知 + 半音 + 百分比 + 未知格式 + 没有音频流可用于外部播放器 + 选择外部播放器画质 + 外部播放器不支持所选串流 + 没有视频流可用于外部播放器 + 不显示下载器尚不支持的串流 + 未知画质 \ No newline at end of file diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 6fa45249e..02d35d384 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -1,45 +1,45 @@ - অনুসন্ধান এ চাপ দিয়ে শুরু করুন - প্রকাশকাল %1$s - কোন স্ট্রিম প্লেয়ার পাওয়া যায়নি। VLC ইনস্টল করতে চান\? - ইনস্টল করুন - বাতিল করুন - ওয়েব ব্রাউজারে ওপেন করুন + শুরু করতে আতস কাঁচটিতে টাচ করুন। + %1$s তারিখে প্রকাশিত + কোন স্ট্রিম প্লেয়ার নেই। VLC ইনস্টল করতে চান\? + ইনস্টল + বাতিল + ব্রাউজারে ওপেন করুন পপ-আপ মোডে ওপেন করো শেয়ার - ডাউনলোউড + ডাউনলোড খুঁজুন সেটিংস - আপনি কি বুঝিয়েছেনঃ %1$s\? + আপনি কি %1$s বুঝিয়েছেন\? শেয়ার করুন বাইরের ভিডিও প্লেয়ার ব্যবহার করুন - বহির্গত অডিও প্লেয়ার ব্যবহার করুন + অন্য অডিও প্লেয়ার ব্যবহার করুন ব্যাকগ্রাউন্ড পপআপ - ভিডিও ডাউনলোড করার ফোল্ডার + ভিডিও ডাউনলোড ফোল্ডার ডাউনলোড করা ভিডিওগুলো এখানে থাকে - ভিডিওগুলির জন্য ডাউনলোডের পাথ নির্বাচন কর + ভিডিও ফাইলের জন্য ডাউনলোড ফোল্ডার বাছুন অডিও ডাউনলোড ফোল্ডার - ডাউনলোড করা অডিও এখানে রাখা হয় - অডিও ফাইলগুলির জন্য ডাউনলোডের ফোল্ডার নির্বাচন করুন - ডিফল্ট রেজোল্যুশন - ডিফল্ট পপআপ রেজোল্যুশন - উচ্চ রেজোল্যুশন দেখাও - শুধুমাত্র কিছু ডিভাইস 2K/4K ভিডিও চালাতে পারে + ডাউনলোড করা অডিও ফাইল এখানে জমা হয় + অডিও ফাইলের জন্য ডাউনলোড ফোল্ডার বাছুন + ডিফল্ট রেজ্যুলেশন + ডিফল্ট পপআপ রেজ্যুলেশন + উচ্চ রেজ্যুলেশন দেখাও + শুধু কিছু ডিভাইস 2K/4K ভিডিও চালাতে পারে Kodi এর মাধ্যমে চালাও হারানো কোর ইনস্টল করবেন\? - দেখাও \"Kodi এর মাধ্যমে চালাও \" বিকল্প - Kodi মিডিয়া সেন্টারে এর মাধ্যমে ভিডিও প্লে করার জন্য একটি বিকল্প প্রদর্শন কর + \"Kodi দিয়ে চালাও\" অপশন দেখাও + Kodi মিডিয়া সেন্টারে এর মাধ্যমে ভিডিও প্লে করার জন্য একটি বিকল্প দেখাও অডিও ডিফল্ট অডিও ফরম্যাট - পছন্দসই ভিডিও ফরম্যাট + ডিফল্ট ভিডিও ফরম্যাট থিম উজ্জ্বল অন্ধকার কালো পপআপ আকার এবং অবস্থান মনে রাখো - শেষ আকার এবং পপআপ সেট অবস্থান মনে রাখো + পপআপের শেষ আকার ও অবস্থান মনে রাখো ডাউনলোড পরবর্তী এবং অনুরূপ ভিডিওগুলি দেখাও URL সমর্থিত নয় @@ -49,7 +49,7 @@ ব্যাকগ্রাউন্ডে চলছে পপআপ মোডে চলছে কন্টেন্ট - বয়স সীমাবদ্ধ কন্টেন্ট দেখাও + বয়সের অনুপযোগী কন্টেন্ট দেখাও লাইভ ডাউনলোডগুলি ডাউনলোডগুলি @@ -76,16 +76,16 @@ তোমার মন্তব্য (ইংরেজিতে): বর্ণনা: - ভিডিও প্রাকদর্শন, সময়ঃ + ভিডিও চালাও, সময়: আপলোডারের ইউজারপিক থাম্বনেইল পছন্দ হয়েছে অপছন্দ হয়েছে ভিডিও অডিও পুনরায় চেষ্টা করো - K + হা M - B + বি শুরু বিরতি @@ -101,18 +101,19 @@ বিস্তারিত জানার জন্য আলতো চাপ অনুগ্রহপূর্বক অপেক্ষা করো… ক্লিপবোর্ডে অনুলিপি করা হয়েছে - অনুগ্রহ করে একটি উপলব্ধ ডাউনলোড ডিরেক্টরি নির্বাচন করো। - এই অনুমতিটি পপআপ মোডে খুলতে প্রয়োজন + পরে সেটিংস থেকে একটি ডাউনলোড ফোল্ডার নির্ধারণ করে দিন + পপআপ মোডে চালু হতে +\nএই অনুমতির প্রয়োজন আছে reCAPTCHA চ্যালেঞ্জ reCAPTCHA চ্যালেঞ্জ অনুরোধ করা হয়েছে কি:\\nঅনুরোধ:\\nকন্টেন্ট ভাষা:\\nসার্ভিস:\\nসময়(GMT এ):\\nপ্যাকেজ:\\nসংস্করণ:\\nওএস সংস্করণ:\\nআইপি পরিসর: - স্ট্রিম ফাইল ডাউনলোড করুন। - তথ্য দেখুন + স্ট্রিম ফাইল ডাউনলোড করুন + তথ্য দেখাও নতুন যা কিছু যুক্ত করুন - খোজ ইতিহাস - ইতিহাস + অনুসন্ধানের ইতিহাস + দেখার ইতিহাস প্লেয়ার ব্যাবহার ইতিহাস @@ -123,13 +124,13 @@ কোন ভিউ নেই নাম পরিবর্তন করুন ওয়েবসাইট - কোন স্ট্রিম প্লেয়ার পাওয়া যায়নি (প্লে করতে VLC ইন্সটল করতে পারেন) - কিছু কিছু রেজোলিউশনে অডিও বন্ধ করে দেয় + কোন স্ট্রিম প্লেয়ার নেই (প্লে করতে VLC ইন্সটল করতে পারেন)। + কিছু কিছু রেজ্যুলেশনে অডিও বন্ধ করে দেয় সাবস্ক্রাইব - সাবস্ক্রাইব করা আছে + সাবস্ক্রাইবকৃত চ্যানেল থেকে আনসাবস্ক্রাইব্ড সাবস্ক্রিপশন পরিবর্তন করা যায়নি - সাবস্ক্রিপশন আপডেটে ব্যার্থ + সাবস্ক্রিপশন আপডেট করা যায়নি সাবস্ক্রিপশন বুকমার্ককৃত প্লেলিস্টসমূহ দ্রুত টানা ব্যাবহার করুন @@ -137,28 +138,28 @@ ট্যাব পছন্দ করুন অনির্দিষ্ট সন্ধান প্লেয়ারকে আরো দ্রুত গতিতে সন্ধান করার সুবিধা দেয়, কিন্তু এটি সম্পূর্ণ নির্ভুল নাও হতে পারে ৷ ৫, ১৫ ও ২৫ সেকেন্ডের জন্য এটা কাজ করবে না ৷ দ্রুত-ফরওয়ার্ড/-পুনরায় সন্ধান সময়কাল - মতামত প্রদর্শন বন্ধ করতে অপশনটি বন্ধ করুন - মতামত প্রদর্শন করুন - থাম্বনেইল লোড করুন + মন্তব্যসমূহ লুকাতে বন্ধ করুন + মন্তব্যসমূহ দেখাও + থাম্বনেইল লোড করো থাম্বনেইল প্রদর্শন বন্ধ করার মাধ্যমে, ডাটা এবং মেমোরি সংরক্ষণ করুন। অপশনটি‌ পরিবর্তনে ইন-মেমোরি এবং অন-ডিস্ক ইমেজ ক্যাশ উভয়ই মুছে যাবে। - ছবির ক্যাশ মুছে ফেলা হয়েছে + ছবির ক্যাশ মোছা হয়েছে সব ক্যাশড ওয়েবপেজ ডেটা মুছে ফেলো - ক্যাশ করা মেটাডেটা মুছো - মেটাডেটা ক্যাশ মুছে ফেলা হয়েছে - পরবর্তী স্ট্রিম স্বয়ংক্রিয়ংভাবে সংযোজন করুন + ক্যাশ করা মেটাডেটা মোছ + মেটাডেটা ক্যাশ মোছা হয়েছে + পরবর্তী স্ট্রিম স্বয়ংক্রিয়ংভাবে সংযোজন করো প্লেয়ারের উজ্জ্বলতা নিয়ন্ত্রণ করতে সংকেত ব্যবহার করো উজ্জ্বলতার নিয়ন্ত্রণ সংকেত প্লেয়ারের ভলিউম নিয়ন্ত্রণ করতে সংকেত ব্যবহার করো ভলিউম সংকেত নিয়ন্ত্রণ - সম্পূর্ণ + সম্পন্ন তালিকা - তালিকাতে পজিশন + তালিকা আকারে সাজাও শেষ প্লেব্যাক পজিশন এ যাও পুনরায় প্লে ব্যাক চালু করো - সার্চগুলো স্থানীয়ভাবে জমা করো - সার্চের সময় পরামর্শ দেখাও - সার্চ পরামর্শ - রেজাল্ট দেখানো হচ্ছেঃ %s + অনুসন্ধানের বিষয়বস্তু স্থানীয়ভাবে জমা করো + অনুসন্ধানকালীন কী পরামর্শ দেওয়া হবে তা বাছুন + অনুসন্ধানের পরামর্শ + ফলাফল দেখাচ্ছে: %s এর জন্যে সম্পর্কিত নিউপাইপ এর সম্বন্ধে ট্রেন্ডিং @@ -175,11 +176,11 @@ পাওয়া যায় নি সার্ভার পাওয়া যায় নি এরর দেখান - ডাউন লোড হয় নি - পজ হয়েছে - ডাউন লোড করার জন্য চাপ দিন + ডাউনলোড ব্যর্থ হয়েছে + স্থগিত + ডাউনলোড করতে টোকা দিন অটো - কোন সীমা নেই + সীমাহীন কোন ক্যাপশন নেই প্লে লিস্ট তৈরি হয়েছে প্লে লিস্ট ডিলিট করতে চান\? @@ -231,15 +232,15 @@ স্ট্রিম টি চালানো গেল না বাহ্যিক স্টোরেজ নেই সাহায্য - সার্চ ইতিহাস ডিলিট হয়েছে। + অনুসন্ধান ইতিহাস মোছা হয়েছে সমগ্র সার্চ ইতিহাস মুছবেন\? সার্চের ইতিহাস মোছা হয় সার্চ ইতিহাস মুছুন - প্লে ব্যাক এর অবস্থান মোছা হয়েছে। + প্লেব্যাক অবস্থানসমূহ মোছা হয়েছে সমস্ত প্লে লিস্ট এর অবস্থান মুছবেন\? সমস্ত প্লে লিস্ট এর অবস্থান মুছে ফেলুন প্লে লিস্ট এর অবস্থান মুছে ফেলুন - দেখার ইতিহাস মুছে গেছে। + দেখা ভিডিওর ইতিহাস মোছা হয়েছে সম্পূর্ণ দেখার ইতিহাস মুছে ফেলুন\? দেখার ইতিহাস মুছে ফেলুন ডাটা বেস এক্সপোর্ট করুন @@ -247,9 +248,9 @@ মেন এ ফিরে যান পপ-আপ এ খুলুন পেছনে নিয়ে যান - নিউ পাইপ এর নতুন ভার্সন এর সূচনা - অ্যাপ আপডেট এর সূচনা - নিউ পাইপ এর সূচনা + নতুন নিউপাইপ ভার্সন এর নোটিফিকেশন + অ্যাপ আপডেট নোটিফিকেশন + নিউ পাইপ নোটিফিকেশন সব চালু করুন ফাইল ডিলিট হয়েছে সেরা রেজুলিউসন @@ -257,7 +258,7 @@ অ্যালবাম গুলি গান গুলি ভিডিও গুলি - YouTube নিষিদ্ধ মোড + YouTube-এর \"সীমিত মোড\" চালু করো শুধুমাত্র HTTPS URL গুলি সাপোর্ট করে ইন্সটান্স এর ইউ আর এল ইন্সটান্স যোগ করুন @@ -295,14 +296,14 @@ সরাও তৈরি করো ফিরে যাও - স্বয়ংক্রিয় + স্বয়ংক্রিয় প্লে পুনরায় চালু করো দেখা ভিডিওগুলোর হিসেব - ডাটা মুছে ফেলুন + ডাটা মুছে ফেল কিছুই না পুনরায় প্রথম ক্রিয়া বোতাম - থাম্বনেলে ১:১ অনুপাতে করো + থাম্বনেল ১:১ অনুপাতে সেট করো সিস্টেম ডিফল্ট এ ফাইলটি চালানোর জন্য কোন অ্যাপ ইন্সটলকৃত নেই প্লেলিস্ট বুকমার্ক করুন @@ -351,4 +352,111 @@ ভাষা বয়স সীমা প্রধান পাতার উপাদান সমূহ + কেউ শুনছে না + মূল প্লেয়ার পুরো পর্দাজুড়ে চালাও + বর্ণনা + প্লেয়ার নোটিফিকেশন + ইউটিউব একটি \"সীমিত ধরন\" সরবরাহ করে যেটি সম্ভাব্য প্রাপ্তবয়স্ক কন্টেন্ট লুকিয়ে রাখে + ভিডিও হ্যাশ নোটিফিকেশন + নতুন স্ট্রিমসমূহ + রিক্যাপচা কুকিজ মোছা হয়েছে + অন্যান্য অ্যাপের উপরে হাজির হতে অনুমতি দিন + সমস্যা হয়েছে, নোটিফিকেশন দেখুন + তৈরিকৃত রিপোর্ট কপি করো + সম্পর্কিত আইটেমসমূহ + ঝিঁঝিঁপোকা ছাড়া এখানে কিছু নেই + পুনর্বিন্যাস্ত করতে টান দিন + গ্রাহকসংখ্যা সুলভ নয় + কেউ দেখছে না + + %s ভিডিও + %s ভিডিও + + + %s জন শ্রোতা + %s জন শ্রোতা + + ∞ ভিডিও + সমাধান করো + নতুন স্ট্রিমের নোটিফিকেশন + যেকোন নেটওয়ার্ক + আপডেটসমূহ + শুধু ওয়াই-ফাই তে + কখনো না + অপেক্ষমাণ + আপডেট চেক করা হচ্ছে … + , + রাত্রিকালীন থিম + বর্ণনা দেখাও + দ্বিতীয় পদক্ষেপ বোতাম + পঞ্চম পদক্ষেপ বোতাম + ভিডিও বর্ণনা ও বাড়তি তথ্য লুকাতে বন্ধ করুন + দেখিও না + নিউ পাইপ আপডেট এসেছে! + মন্তব্যসমূহ নিষ্ক্রিয় আছে + + %s বার দেখেছে + %s বার দেখেছে + + ওভাররাইট + তৃতীয় পদক্ষেপ বোতাম + স্থানীয় অনুসন্ধানের পরামর্শ + মোবাইল ডেটা ব্যবহারের সময় রেজুলেশন সীমিত করো + অপেক্ষমাণ + এক প্লেয়ার বদলে অন্য প্লেয়ারে যাওয়া আপনার অপেক্ষমাণ সারিকে প্রতিস্থাপন করতে পারে + সক্রিয় প্লেয়ারের অপেক্ষমাণ সারি প্রতিস্থাপিত হবে + বিঘ্ন ঘটার পরে চালানো অব্যাহত রাখো (যেমন. ফোনকল) + প্লেব্যাক অবস্থানের চিহ্নসমূহ তালিকায় দেখাও + URL টি বোঝা যায়নি। অন্য অ্যাপ দিয়ে খুলবো\? + কনটেন্টের জন্য পূর্বনির্ধারিত দেশ + বাইরের প্লেয়ারসমূহ এ ধরনের লিঙ্কসমূহ সমর্থন করে না + হ্যাশ হিসাব করা হচ্ছে + আপডেট চেক করো + + %s জন দেখছে + %s জন দেখছে + + + %s টি নতুন স্ট্রিম + %s টি নতুন স্ট্রিম + + + ডাউনলোড সম্পন্ন + %s ডাউনলোড হয়েছে + + ১০০+ ভিডিও + ব্যবহারকারীগণ + + %s জন গ্রাহক + %s জন গ্রাহক + + আনুষঙ্গিক তথ্য দেখাও + নেটওয়ার্ক সংযোগ দরকার + উচ্চ মান (বড় আকারের) + নিম্ন মান (ছোট আকার) + চতুর্থ পদক্ষেপ বোতাম + এলোমেলো + বাফারিং + নোটিফিকেশন বর্ণিল করো + এই ভিডিও বয়স দ্বারা সীমিত। +\n +\nএটি দেখতে চাইলে সেটিংস থেকে \"%1$s\" চালু করুন। + সব টগল করো + আপনি এখন এই চ্যানেলের গ্রাহক + অপেক্ষমাণ সারি বাদ দেওয়ার আগে নিশ্চিত হতে জিজ্ঞেস করো + শিশুদের জন্য বয়সসীমার কারণে অনুপযোগী হতে পারে এমন কন্টেন্ট দেখাও (যেমন ১৮+) + প্লে করা স্ট্রিমসমূহের ইতিহাস এবং প্লেব্যাক অবস্থানসমূহ মুছে দেবে + নোটিফিকেশনসমূহ + ত্রুটি প্রতিবেদন এর নোটিফিকেশন + ইতিহাস, সাবস্ক্রিপশন্স, প্লেলিস্ট ও সেটিংস রপ্তানি করো + কোনো ডাউনলোড ফোল্ডার ঠিক করা নেই, এখন ডিফল্ট ডাউনলোড ফোল্ডার বাছুন + স্বয়ংক্রিয় (ডিভাইস থিম) + প্রিয় রাত্রিকালীন থিম বেছে নিন — %s + লাইসেন্স + গোপনতা + নিচ থেকে আপনার পছন্দের রাত্রিকালীন থিম বেছে নিতে পারেন + ডাউনলোড শুরু হয়েছে + দেখা হিসেবে মার্ক করো + ট্যাগসমূহ + অ্যান্ড্রয়েডকে থাম্বনেইলের প্রধান রং অনুযায়ী রঙিন করুন (উল্লেখ্য যে, এটি সব ডিভাইসে উপলব্ধ নয়) \ No newline at end of file diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml index 24498f162..00468f7f7 100644 --- a/app/src/main/res/values-bn-rIN/strings.xml +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -33,7 +33,7 @@ ভিডিও প্রাকদর্শন, সময়ঃ বর্ণনা: আপনার মন্তব্য (ইংরেজিতে): - কি:\\nঅনুরোধ:\\nকন্টেন্ট ভাষা:\\nসার্ভিস:\\nসময়(GMT এ):\\nপ্যাকেজ:\\nসংস্করণ:\\nওএস সংস্করণ:\\nআইপি পরিসর: + কি:\\nঅনুরোধ:\\nকন্টেন্ট ভাষা:\\nকন্টেন্ট দেশ:\\nঅ্যাপ ভাষা:\\nসার্ভিস:\\nসময়(GMT এ):\\nপ্যাকেজ:\\nসংস্করণ:\\nওএস সংস্করণ: কি হয়েছিল: তথ্য: প্রতিবেদন @@ -83,7 +83,7 @@ সব ক্যাশড ওয়েবপেজ ডেটা মুছে ফেলো ক্যাশ করা মেটাডেটা মুছো ছবির ক্যাশ মুছে ফেলা হয়েছে - থাম্বনেইল প্রদর্শন বন্ধ করার মাধ্যমে, ডাটা এবং মেমোরি সংরক্ষণ করুন। অপশনটি‌ পরিবর্তনে ইন-মেমোরি এবং অন-ডিস্ক ইমেজ ক্যাশ উভয়ই মুছে যাবে। + থাম্বনেইল প্রদর্শন বন্ধ করার মাধ্যমে, ডাটা এবং মেমোরি সংরক্ষণ করুন। অপশনটি‌ পরিবর্তনে ইন-মেমোরি এবং অন-ডিস্ক ইমেজ ক্যাশ উভয়ই মুছে যাবে মতামত প্রদর্শন বন্ধ করতে অপশনটি বন্ধ করুন মতামত প্রদর্শন করুন থাম্বনেইল লোড করুন @@ -142,7 +142,7 @@ কোন স্ট্রিম প্লেয়ার পাওয়া যায়নি (প্লে করতে VLC ইন্সটল করতে পারেন)। কোন স্ট্রিম প্লেয়ার পাওয়া যায়নি। VLC ইনস্টল করতে চান\? প্রকাশকাল %1$s - অনুসন্ধান এ চাপ দিয়ে শুরু করুন + অনুসন্ধান শুরু করুন নতুন নতুন কি অ্যাপ এর ভাষা @@ -224,7 +224,7 @@ প্লেয়ার এর এরর থেকে বেরিয়ে আসুন স্ট্রিম টি চালানো গেল না সার্চের ইতিহাস মোছা হয় - প্লে ব্যাক এর অবস্থান মোছা হয়েছে। + প্লে ব্যাক এর অবস্থান মোছা হয়েছে সমস্ত প্লে লিস্ট এর অবস্থান মুছবেন\? সমস্ত প্লে লিস্ট এর অবস্থান মুছে ফেলুন প্লে লিস্ট এর অবস্থান মুছে ফেলুন @@ -300,4 +300,20 @@ প্রথম অ্যাকশান বোতাম দ্বিতীয় অ্যাকশান বোতাম নতুন স্ট্রিম + শুধুমাত্র ওয়াইফাইএ + ভাষা + URL বোঝা যায় নি, অন্য অ্যাপ এ খুলুন\? + ফাইল তৈরি করা যাচ্ছে না + ভিডিও বিবরণ ও বাড়তি তথ্য বন্ধ করুন + মূল প্লেয়ার ফুল স্ক্রীন এ শুরু করুন + " " + সার্ভার এর সাথে যোগাযোগ করা যাচ্ছে না + বাফার হচ্ছে + ডাউনলোড শুরু হয়েছে + এটি দিয়ে খুলুন + দেখা হিসাবে চিহ্নিত করুন + থাম্বনেল 1:1 আকৃতির অনুপাতের করুন + বিজ্ঞপ্তিতে প্রদর্শিত ভিডিও থাম্বনেল 16:9 থেকে 1:1 অনুপাতের করুন (বিকৃতি দেখা যেতে পারে) + অদলবদল + কিছু না \ No newline at end of file diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 0f1ea9c25..a1acf9fd1 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -44,7 +44,7 @@ সবসময় জিজ্ঞেস করুন ভিডিও প্লেয়ার ড্রয়ার বন্ধ করুন - ড্রয়ার খুলন + ড্রয়ার খুলো অডিও সেটিং বিবরণ সরাও @@ -126,7 +126,7 @@ গিটহাব এ এরর রিপোর্ট করুন মেইলের মাধ্যমে ত্রুটি প্রতিবেদন করুন দুঃখিত, এটা ঘটা উচিত ছিল না। - আপনি কি ডিফল্ট এ ফিরতে চান\? + তুমি কি এই সহজাত পছন্দ ফিরত চাও\? ডিফল্ট এ ফিরে যান ডাউন লোড এর জন্য কোন স্ট্রিম নেই একটা এরর হয়েছেঃ %1$s @@ -147,15 +147,15 @@ বাহ্যিক স্টোরেজ নেই ত্রুটি সাহায্য - সার্চ ইতিহাস ডিলিট হয়েছে। + সার্চ ইতিহাস ডিলিট হয়েছে সমগ্র সার্চ ইতিহাস মুছবেন\? সার্চের ইতিহাস মোছা হয় সার্চ ইতিহাস মুছুন - প্লে ব্যাক এর অবস্থান মোছা হয়েছে। + প্লে ব্যাক এর অবস্থান মোছা হয়েছে সমস্ত প্লে লিস্ট এর অবস্থান মুছবেন\? সমস্ত প্লে লিস্ট এর অবস্থান মুছে ফেলুন প্লে লিস্ট এর অবস্থান মুছে ফেলুন - দেখার ইতিহাস মুছে গেছে। + দেখার ইতিহাস মুছে গেছে সম্পূর্ণ দেখার ইতিহাস মুছে ফেলুন\? দেখার ইতিহাস মুছে ফেলুন ডাটা বেস এক্সপোর্ট করুন @@ -189,7 +189,7 @@ ডাউনলোডগুলি ডাউনলোডগুলি লাইভ - YouTube \"নিষিদ্ধ মোড\" চালু করুন + ইউটিউব ‘সীমিত মোড’ চালু করো বয়স সীমাবদ্ধ কন্টেন্ট দেখাও কন্টেন্ট পপআপ মোডে চলছে @@ -231,12 +231,12 @@ সব ক্যাশড ওয়েবপেজ ডেটা মুছে ফেলো ক্যাশ করা মেটাডেটা মুছো ছবির ক্যাশ মুছে ফেলা হয়েছে - থাম্বনেইল প্রদর্শন বন্ধ করার মাধ্যমে, ডাটা এবং মেমোরি সংরক্ষণ করুন। অপশনটি‌ পরিবর্তনে ইন-মেমোরি এবং অন-ডিস্ক ইমেজ ক্যাশ উভয়ই মুছে যাবে। + থাম্বনেইল প্রদর্শন বন্ধ করার মাধ্যমে, ডাটা এবং মেমোরি সংরক্ষণ করুন। অপশনটি‌ পরিবর্তনে ইন-মেমোরি এবং অন-ডিস্ক ইমেজ ক্যাশ উভয়ই মুছে যাবে মতামত প্রদর্শন বন্ধ করতে অপশনটি বন্ধ করুন মতামত প্রদর্শন করুন থাম্বনেইল লোড করুন দ্রুত-ফরওয়ার্ড/-পুনরায় সন্ধান সময়কাল - অনির্দিষ্ট সন্ধান প্লেয়ারকে আরো দ্রুত গতিতে সন্ধান করার সুবিধা দেয়, কিন্তু এটি সম্পূর্ণ নির্ভুল নাও হতে পারে ৷ ৫, ১৫ ও ২৫ সেকেন্ডের জন্য এটা কাজ করবে না ৷ + অনির্দিষ্ট সন্ধান, চালককে আরো দ্রুত গতিতে সন্ধান করার সুবিধা দেয়, কিন্তু এটি সম্পূর্ণ নির্ভুল নাও হতে পারে ৷ ৫, ১৫ ও ২৫ সেকেন্ডের জন্য এটা কাজ করবে না। দ্রুত টানা ব্যাবহার করুন শেষ আকার এবং পপআপ সেট অবস্থান মনে রাখো পপআপ আকার এবং অবস্থান মনে রাখো @@ -253,12 +253,12 @@ থাম্বনেলে ১:১ অনুপাতে করো Kodi মিডিয়া সেন্টারে এর মাধ্যমে ভিডিও প্লে করার জন্য একটি বিকল্প প্রদর্শন কর \"Kodi দ্বারা চালান\" বিকল্পটি প্রদর্শন কর - হারানো কোর ইনস্টল করবেন\? + হারানো কোর ইনস্টল করবে\? Kodi দ্বারা চালাও শুধুমাত্র কিছু ডিভাইস 2K/4K ভিডিও চালাতে পারে উচ্চতর রেজুলেশন প্রদর্শন করা হবে - ডিফল্ট পপ-আপ রেজোল্যুশন - ডিফল্ট রেজোল্যুশন + সহজাত ভাসমান আকার + সহজাত আকার অডিও ফাইলগুলির জন্য ডাউনলোডের ফোল্ডার নির্বাচন করুন ডাউনলোড করা অডিও ফাইলগুলি এখানে সঞ্চিত থাকে অডিও ডাউনলোড ফোল্ডার @@ -283,20 +283,20 @@ বাইরের ভিডিও প্লেয়ার ব্যবহার করুন শেয়ার করুন রেজাল্ট দেখানো হচ্ছেঃ %s - আপনি কি বুঝিয়েছেনঃ %1$s\? + তুমি কি বুঝিয়েছো ‘%1$s’\? সেটিংস খুঁজুন স্ট্রিম ফাইল ডাউনলোড করুন ডাউনলোউড শেয়ার - পপ-আপ মোডে ওপেন করো - ব্রাউজারে ওপেন করো + ভাসমান অবস্থায় খুলো + ব্রাউজারে খুলো বাতিল ইনস্টল - কোন স্ট্রিম প্লেয়ার পাওয়া যায়নি (প্লে করতে VLC ইন্সটল করতে পারেন). + কোনো ধারা চালক পাওয়া যায়নি (প্লে করতে VLC ইন্সটল করতে পারো)। কোন স্ট্রিম প্লেয়ার পাওয়া যায়নি। VLC ইনস্টল করতে চান\? প্রকাশকাল %1$s - \"অনুসন্ধান\" এ চাপ দিয়ে শুরু করুন + আতশী কাঁচে টিপ দিয়ে শুরু করো। বাফারিং সাফল পঞ্চম অ্যাকশন বাটন @@ -308,7 +308,7 @@ এক প্লেয়ার থেকে অন্য প্লেয়ারে পরিবর্তন করলে তোমার সারি প্রতিস্থাপিত হতে পারে কিউ মোছার আগে নিশ্চিত করো কমপ্যাক্ট বিজ্ঞপ্তিতে প্রদর্শন করতে তুমি সর্বাধিক তিনটি ক্রিয়া নির্বাচন করতে পারো! - নিচের প্রতিটি প্রজ্ঞাপন ক্রিয়া সম্পাদনা করো। ডান দিকের চেকবাক্স ব্যবহার করে কম্প্যাক্ট নোটিফিকেশনে দেখানোর জন্য তিনটি পর্যন্ত নির্বাচন করো। + নিচের প্রতিটি প্রজ্ঞাপন ক্রিয়া সম্পাদনা করো। ডান দিকের চেকবাক্স ব্যবহার করে কম্প্যাক্ট নোটিফিকেশনে দেখানোর জন্য তিনটি পর্যন্ত নির্বাচন করো ১৬:৯ থেকে ১:১অনুপাতে প্রদর্শিত ভিডিও থাম্বনেইল পরিবর্তন করো (বিকৃতি প্রবর্তন করতে পারে) ফিড ওভাররাইট @@ -441,7 +441,7 @@ অধ্যায় মতামত বর্ণনা - দিয়ে খুলুন + দিয়ে খুলো ফিড হালনাগাদ সীমা খালি গ্রুপ নাম কোনো সদস্যতা নির্বাচিত হয়নি @@ -454,7 +454,7 @@ শুধুমাত্র ওয়াই-ফাই-তে নীরবতার সময় দ্রুত আগাও প্লেব্যাক গতি নিয়ন্ত্রণ - পছন্দসই \'মুক্ত\' ক্রিয়া + পছন্দসই \'খোলার\' ক্রিয়া আউট-অফ-লাইফসাইকেল ত্রুটি প্রতিবেদন করো প্লে-তালিকার থাম্বনেইল পরিবর্তিত হয়েছে। অনুরোধকৃত তথ্য লোড হচ্ছে @@ -484,14 +484,14 @@ চ্যানেলের অবতারের প্রতিচ্ছবি দ্রুত মোড বন্ধ করো দ্রুত মোড চালু করো - ফাইল বামানো যায়নি + ফাইল বানানো যায়নি মোবাইল ডাটা ব্যবহারের সময় আকার সীমিত রাখো ভুক্তিতে আসল সময় দেখাও রেডিও বিশেষ সমাধান করো শেষ হালনাগাদের পর একটি সাবস্ক্রিপশনের আগের সময় সেকেলে বিবেচিত — %s - আপনি কি এ গ্রুপটি মুছতে চান\? + তুমি কি এ গ্রুপটি মুছতে চাও\? আরও তথ্য এবং খবরের জন্য নিউপাইপ ওয়েবসাইট দেখো। এ ফাইলটি চালানোর জন্য কোন অ্যাপ ইন্সটলকৃত নেই এতে তোমার বর্তমান অবস্থা সরানো হবে। @@ -516,14 +516,14 @@ পছন্দসমূহ কি আমদানি করতে চাও\? অবৈধ অক্ষরগুলো এই মান দ্বারা প্রতিস্থাপিত অন্য অ্যাপের উপরে দেখাতে অনুমতি দাও - %s-এ আপনার পছন্দের ইন্সট্যান্স খুঁজুন + %s-এ তোমার পছন্দের ইন্সট্যান্স খুজো প্লে করা স্ট্রিমের ইতিহাস এবং প্লেব্যাক অবস্থানগুলি মুছে দেয় এই ভিডিওটি বয়সসীমাবদ্ধ । \n -\nআপনি এটি দেখতে চাইলে সেটটিংসে \"%1$s\" চালু করুন । +\nতুমি এটি দেখতে চাইলে পছন্দসমূহে \"%1$s\" চালু করো। Youtube একটি \"সীমাবদ্ধ মোড\" সরবরাহ করে যা সম্ভাব্য বয়সসীমাবদ্ধ বিষয়গুলি গুপ্ত রাখে শিশুদের জন্যে সম্ভবত অনুপযুক্ত বিষয়গুলোও দেখান যেগুলির একটি বয়সসীমা রয়েছে (যেমন ১৮+ বিষয়সমূহ) - ইউআরএলটি চিন্থিত করা যায়নি | অন্য এপ্লিকেশন এ খুলতে চান \? + ইউআরএলটি চিহ্নিত করা যায়নি। অন্য অ্যাপ্লিকেশনে খুলতে চাও\? এই ফাইলে কাজ করার সময় নিউপাইপ বন্ধ করা হয়েছে এই নামের একটি ডাউনলোড প্রক্রিয়ারত সংরক্ষিত ট্যাব পড়া যায় নি, তাই সহজাতটি ব্যবহার করা হচ্ছে @@ -543,7 +543,7 @@ অনুসন্ধান ইতিহাস থেকে এই ভুক্তিটি মুছবে\? প্রত্যেক ডাউনলোড কোথায় রাখা হবে তা জিজ্ঞেস করা হবে এই নামের একটি ডাউনলোড চলমান - অ্যাপ আবার শুরু হলে ভাষা পাল্টাবে। + অ্যাপ আবার শুরু হলে ভাষা পাল্টাবে মিডিয়া সুরঙ্গকরণ অক্ষম দ্রুত ফিড অবস্থা এ বিষয়ে এর বেশি তথ্য দেয় না। কোনো ডাউনলোড ফোল্ডার নির্দিষ্ট করা হয়নি, এখনই একটা সহজাত ডাউনলোড ফোল্ডার নির্বাচন করো @@ -555,7 +555,7 @@ বর্ণনার লেখা নির্বাচন করা সক্ষম করো %s এই কারণ বলছে: প্রক্রিয়াকরণ ফিডে ত্রুটি - মুক্ত ওয়েবসাইট + ওয়েবসাইট খুলুন অ্যাকাউন্ট ধ্বংসকৃত প্রতিচ্ছবি সংযোগ বয়সসীমা @@ -582,4 +582,44 @@ %sটি ডাউনলোড সমাপ্ত দেখা হয়েছে চিহ্নিত করো + চালক বিজ্ঞপ্তি + নিম্ন মান(ছোট) + মূল তৈরিকারকের পছন্দ করা + , + বিজ্ঞপ্তি পাঠাও + সব পরিবর্তন করো + উচ্চতর মান (বৃহত্তর) + মন্তব্য নিষ্ক্রিয় + স্থানীয় অনুসন্ধানের পরামর্শ + দূর অনুসন্ধানে পরামর্শ + শতাংশ + সেমিটোন + বাইরের চালক সহজাত + পরেরটা ক্রমে রাখো + চালক থামাও + নতুন ধারা + কম্পাঙ্ক দেখো + পূর্বদর্শন রেখার মাধ্যমে প্রাকদর্শন + ছবিরূপ সূচক দেখাও + দেখিও না + যেকোনো নেটওয়ার্ক + পরেরটা ক্রমে রাখা হয়েছে + পিনকৃত মন্তব্য + বিজ্ঞপ্তি + হালনাগাদ দেখা হচ্ছে … + হালনাগাদ আছে কিনা দেখো + মূল প্লেয়ার ফুল স্ক্রীন এ শুরু করুন + ধারার নতুন ভুক্তি + ত্রুটি প্রতিবেদন এর বিজ্ঞপ্তি + পটভূমি বা ভিডিওর ‘বিস্তারিত:’ এর ভাসমান বোতাম টিপলে একটা তথ্য দেখাও + + %s টি নতুন ধারা + %s টি নতুন ধারা + + প্রতিচ্ছবিত প্রধান রঙ অনুসারে অ্যান্ড্রয়েডকে বিজ্ঞপ্তির রঙ কাস্টমাইজ করতে দাও (দ্রষ্টব্য যে এটা সমস্ত ডিভাইসে উপলব্ধ নয়) + অজ্ঞাত ফরম্যাট + বাহ্যিক প্লেয়ারের জন্য মান নির্বাচন করুন + বাহ্যিক প্লেয়ারের জন্য কোনো অডিও স্ট্রিম নেই + বাহ্যিক প্লেয়ারের জন্য কোনো ভিডিও স্ট্রিম নেই + অজ্ঞাত মান \ No newline at end of file diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 421139875..a42e68940 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -650,7 +650,6 @@ Inicia el reproductor principal en pantalla completa Llisqueu els elements per eliminar-los Si la rotació automàtica està bloquejada, no inicieu vídeos al mini reproductor, sinó que aneu directament al mode de pantalla completa. Podeu accedir igualment al mini reproductor sortint de pantalla completa - Ja s\'està reproduint en segon pla Notificació d\'informe d\'error Tancar abruptament el reproductor Comprovar si hi ha actualitzacions diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml index f621b88bd..9796404b4 100644 --- a/app/src/main/res/values-ckb/strings.xml +++ b/app/src/main/res/values-ckb/strings.xml @@ -11,7 +11,7 @@ \nیوتیوب نموونەیە لەم خزمەتگوزارییە کە ڕێگەی خێرا بەکاردەبات بەهۆی پیشاندەری RSS. \n \nبۆیە هەڵژرادن بۆ خۆت دەگەڕێتەوە: زانیاری تەواو یان خێرا.
- نیوپایپ نه‌رمه‌والایه‌كی سەرچاوە کراوەیە : دەتوانیت بەکاریبهێنیت، بیخوێنیتەوە و هاوبەشی پێبکەیت و بەرەوپێشی ببەیت. بەتایبەتی دەتوانی دابەشیبکەیتەوە یاخوود بگۆڕیت بەپێی مەرجەکانی GNU مۆڵەتنامەی گشتی وەک نه‌رمه‌واڵایه‌كی بڵاوی خۆڕایی, بەهۆی وەشانی ٣ ی مۆڵەتنامە، یان هەر وەشانێکی دوواتر. + نیوپایپ نه‌رمه‌والایه‌كی سەرچاوە کراوەیە : دەتوانیت بەکاریبهێنیت، بیخوێنیتەوە، هاوبەشی پێبکەیت ،بەرەوپێشی ببەیت. بەتایبەتی دەتوانی دابەشیبکەیتەوە یاخوود بگۆڕیت بەپێی مەرجەکانی GNU مۆڵەتنامەی گشتی وەک نه‌رمه‌واڵایه‌كی بڵاوی خۆڕایی, بەهۆی وەشانی ٣ ی مۆڵەتنامە، یان هەر وەشانێکی دوواتر. چی:\\nداواكاری:\\nزمانی بابەت:\\nوڵاتی بابەت:\\nزمانی به‌رنامه‌:\\nخزمه‌تگوزاری:\\nGMT كات:\\nپاكێج:\\nوه‌شان:\\nOS وه‌شان: پڕۆژەی نیوپایپ زانیارییە تایبەتییەکانت بە وردی دەپارێزێت. هەروەها به‌رنامه‌كه‌ هیچ زانایارییەکت بەبێ ئاگاداری تۆ بەکارنابات. \n‫سیاسەتی تایبەتی نیوپایپ بە وردی ڕوونکردنەوەت دەداتێ لەسەر ئەو زانیاریانەی وەریاندەگرێت و بەکاریاندەبات. @@ -672,7 +672,6 @@ پیشاندانی ”کڕاش کردنی لێدەرەکە“ سازاندنی پەیامی کێشەیەک پشکنین بۆ نوێکردنەوە - وا لە پاشبنەمادا لێدەدرێت کێشە لە سکاڵا کردنی پەیام پەیامەکانی سکاڵاکردن لە کێشەکان بابەتە نوێیەکانی فیید @@ -684,4 +683,29 @@ LeakCanary بەردەست نییە هیچ ڕێکخەرێکی فایلی گونجاو نەدۆزرایەوە بۆ ئەم کردارە. \nتکایە ڕێکخەرێکی فایلی دابمەزرێنە کە گونجاوبێت لەگەڵ دەسەڵاتی گەیشتن بە بیرگە. + پشکنین کردن بۆ پەخشی نوێ + پەیامەکانی پەخشە نوێیەکان + پەیام بکرێم لەکاتی هەبوونی پەخشی نوێی بەژدارییەکان + فریکوێنسی دەپشکنرێت + پەیوەندی تۆڕ داواکراوە + هەر تۆڕێک + تۆ ئێستا ئەم چەناڵەت بەژداری کردووە + ، + پەخشە نوێیەکان + پەیامی لێدەر + پەیامەکان + پەیامەکان بۆ پەخشە نوێیەکانی بەژدارییەکانت + وردەکاری پەخش باردەکرێت… + + %s پەخشی نوێ + %s پەخشانی نوێ + + پەیامی ئێستای لێدانی پەخش ڕێکبخە + هەموو فایلە دابەزێنراوەکان لە دیسک بسڕدرێتەوە؟ + پەیامەکان ناکاراکراون + پەیامم بکە + "قەبارەی نێوان بارکردنەکە بگۆڕە (لە ئێستادا %s) . بەهایەکی کەمتر لەوانەیە بارکردنی ڤیدیۆی سەرەتایی خێراتر بکات. گۆڕانکارییەکان پێویستیان بە داگیرساندنەوەی لێدەر هەیە" + لەسەدا + نیمچەتەن + بنەڕەتی ExoPlayer \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 84c957f1c..ea6d292d8 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -684,7 +684,6 @@ Vytvořit oznámení o chybě Kontrola aktualizací… Ukázat „Shodit přehrávač“ - Hraje již v pozadí Nové položky feedů Pro tuto akci nebyl nalezen žádný vhodný správce souborů. \nProsím, nainstalujte správce souborů kompatibilní se Storage Access Framework. @@ -698,7 +697,30 @@ Shodit přehrávač Změnit interval načítání (aktuálně %s). Menší hodnota může zrychlit počáteční načítání videa. Změna vyžaduje restart přehrávače. LeakCanary není dostupné - Upravit výšku tónů po půltónech - Krok tempa Výchozí ExoPlayer + Nastavit oznámení k právě přehrávanému strýmu + Oznámení o nových strýmech + Oznámit o nových strýmech k objednání + Frekvence kontroly + Jakákoli síť + Nutné síťové připojení + Smazat všechny stažené soubory z disku\? + Objednali jste si nyní tento kanál + Všechny přepnout + Nové strýmy + Oznámení o nových strýmech k objednání + Spustit kontrolu nových strýmů + Oznámení přehrávače + Oznámení + Načítám podrobnosti o strýmu… + Oznámení jsou vypnuta + Přijímat oznámení + , + + %s nový strým + %s nové strýmy + %s nových strýmů + + Procento + Půltón \ No newline at end of file diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 71bb83fb0..fc16fbd63 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -1,8 +1,8 @@ Tryk på forstørrelsesglasset for at komme i gang. - Udgivet %1$s - Ingen streamafspiller blev fundet. Installer VLC\? + Udgivet den %1$s + Ingen streamafspiller blev fundet. Installér VLC\? Ingen streamafspiller fundet (du kan installere VLC for at afspille den). Installer Annuller @@ -13,13 +13,13 @@ Download stream-fil Søg Indstillinger - Mente du: %1$s\? + Mente du \"%1$s\"\? Del med Benyt ekstern videoafspiller - Fjerner lyd ved NOGLE opløsninger + Fjerner lyd ved nogle opløsninger Brug ekstern lydafspiller Abonner - Abonneret + Abonnerer Afmeld abonnement Abonnement afmeldt Kunne ikke ændre abonnement @@ -33,17 +33,17 @@ Pop op Føj til Placering af videodownloads - Mappe som videoer skal downloades til - Angiv downloadmappe for videoer + Downloadede videoer gemmes her + Angiv downloadmappe for videofiler Downloadmappe for lydfiler - Downloadede lydfiler bliver gemt her + Downloadede lydfiler gemmes her Angiv downloadmappe for lydfiler Standardopløsning Standardopløsning for pop op Vis højere opløsninger - Ikke alle enheder understøtter afspilning af 2K/4K-videoer + Kun nogle enheder kan afspille 2K-/4K-videoer Afspil med Kodi - Kore-appen ikke fundet. Installer den\? + Installer manglede Kore-app\? Vis valgmuligheden \"Afspil med Kodi\" Vis en knap til at afspille en video via Kodi Lyd @@ -56,30 +56,30 @@ Husk størrelse og placering af pop op Husk sidste størrelse og placering af pop op-afspiller Brug hurtig og upræcis søgning - Upræcis søgning lader afspilleren finde placeringer hurtigere, men mindre præcist + Upræcis søgning lader afspilleren finde placeringer hurtigere, men mindre præcist. Søgninger på 5, 15 eller 25 sekunder fungerer ikke med denne indstilling, slået til Indlæs miniaturebilleder - Slå fra for at undgå indlæsning af billeder, hvorved der spares data og hukommelse. Ændringer sletter billedcachen i både ram og lager. + Slå fra for at undgå indlæsning af billeder, hvorved der spares data og hukommelse. Ændringer sletter billedcachen i både ram og lager Billedcache slettet Slet metadata-cachen Slet alle websidedata fra cachen Metadata-cache slettet Føj automatisk næste stream til køen - Føj automatisk relaterede streams til køen når den sidste stream i en ikke-repeterende kø afspilles. + Fortsæt nedlukningen af en (ikke-gentagende) playback kø ved at tilføje et relateret stream Juster lydstyrke ved hjælp af fingerbevægelser - Brug fingerbevægelser til at kontrollere lydstyrke + Brug fingerbevægelser til at kontrollere afspillerens lydstyrke Styr lysstyrken med fingerbevægelser Brug fingerbevægelser til at justere afspillerens lysstyrke Søgeforslag - Vis forslag når der søges + Vælg forslagene, der vises, når der søges Søgehistorik Gem søgninger lokalt - Historik og cache + Visningshistorik Husk sete videoer - Fortsæt når appen kommer i fokus + Fortsæt afspilning Fortsæt afspilning efter afbrydelser (fx telefonopkald) Download Vis \'Næste\' og \'Lignende\' videoer - Vis \"Hold for at tilføje\"-tip + Vis \"Hold for at sætte i kø\"-tip Vis et tip når der trykkes på baggrunds- eller pop op-knappen på siden med videodetaljer Denne webadresse er ikke understøttet Standardland for indhold @@ -94,8 +94,8 @@ Afspiller i baggrunden Afspiller i pop op-tilstand Indhold - Aldersbegrænset indhold - LIVE + Vis aldersbegrænset indhold + Live Downloads Downloads Fejlrapport @@ -117,29 +117,29 @@ Altid Kun én gang Fil - NewPipe-notifikation - Notifikationer for NewPipes baggrunds- og pop op-afspillere + NewPipe notifikation + Notifikationer for NewPipes afspiller Notifikation om opdatering af app - Notifikationer for nye NewPipe-versioner + Notifikationer for nye NewPipe versioner [Ukendt] Skift til baggrund Skift til pop op Skift til hovedafspiller Importer database Eksporter database - Overskriver din nuværende historik og abonnementer - Eksporter historik, abonnementer og spillelister + Overskriver din nuværende historik, abonnementer, spillelister og (hvis det ønskes) indstillinger + Eksporter historik, abonnementer, spillelister og indstillinger Slet visningshistorik - Sletter historikken for viste videoer + Sletter historikken og positioner af tidligere viste videoer Slet hele visningshistorikken\? - Visningshistorikken blev slettet. + Visningshistorikken blev slettet Slet søgehistorik Sletter historikken for søgeord Slet hele søgehistorikken\? - Søgehistorik slettet. + Søgehistorikken blev slettet Fejl Eksternt lager utilgængeligt - Download til eksternt SD-kort er endnu ikke muligt. Nulstil placering af download-mappe\? + Det er endnu ikke muligt at downloade til et eksternt SD-kort. Nulstil download-mappens placering\? Netværksfejl Kunne ikke indlæse alle miniaturebilleder Kunne ikke dekryptere URL-signatur for video @@ -161,16 +161,16 @@ Ingen streams er tilgængelige for download Bruger standardfaner pga. fejl ved indlæsning af gemte faner Genskab standardindstillinger - Vil du genskabe standardindstillingerne\? + Vil du genoprette standardindstillingerne\? Undskyld, dette skulle ikke være sket. - Rapporter fejl via e-mail - Undskyld, nogle fejl opstod. - RAPPORTER + Rapporter denne fejl via e-mail + Beklager, noget gik galt. + Rapporter Information: Hvad skete der: Din kommentar (på engelsk): Detaljer: - Videominiaturebillede + Afspil video, længde: Uploaders profilbillede Synes godt om Kan ikke lide @@ -201,8 +201,9 @@ Tryk for detaljer Vent venligst… Kopieret til udklipsholderen - Vælg venligst en tilgængelig downloadmappe - Denne tilladelse er nødvendig for at kunne åbne i pop op-tilstand + Vælg senere en tilgængelig downloadmappe i indstillingerne + Denne tilladelse behøves for +\nat åbne i pop op-tilstand 1 element slettet. reCAPTCHA-udfordring Der blev anmodet om en reCAPTCHA-udfordring @@ -223,7 +224,7 @@ Hvad enten du har idéer til oversættelse, designændringer, kodeoprydning eller virkelig tunge kodeændringer, så er hjælp altid velkommen. Jo mere der bliver gjort, jo bedre bliver det! Se på GitHub Doner - NewPipe er udviklet af frivillige der bruger tid på at give dig den bedste oplevelse. Giv noget tilbage for at hjælpe NewPipes udviklere til at gøre appen endnu bedre, mens de nyder en kop kaffe. + NewPipe er udviklet af frivillige, der bruger deres fritid på at give dig den bedst mulige brugeroplevelse. Giv noget tilbage for at hjælpe NewPipes udviklere til at gøre appen endnu bedre, mens de nyder en kop kaffe. Giv noget tilbage Websted Besøg NewPipes websted for mere information og nyheder. @@ -292,7 +293,7 @@ Færdig Afventning efterbehandling - + Læg i kø Handling afvist af systemet Download fejlede Generer unikt navn @@ -303,9 +304,9 @@ Vis fejl Filen kan ikke oprettes Destinationsmappen kan ikke oprettes - Sikker forbindelse fejlede + Kunne ikke etablere en sikker forbindelse Kunne ikke finde serveren - Kan ikke forbinde til serveren + Kan ikke oprette forbindelse til serveren Serveren sender ikke data Serveren accepterer ikke multitrådede downloads; prøv igen med @string/msg_threads = 1 Ikke fundet @@ -313,7 +314,7 @@ Stop Hændelser Intet at se her - T + t mio. mia. @@ -324,18 +325,18 @@ Kunne ikke importere abonnementer Kunne ikke eksportere abonnementer Konferencer - Start her når i baggrunden - Start her ved ny pop op + Start afspilningen i baggrunden + Start afspilning i et pop op Åbn skuffe Luk skuffe - Hvad:\\nForespørgsel:\\nIndholdssprog:\\nTjeneste:\\nGMT-tid:\\nPakke:\\nVersion:\\nOS-version: - Standardhandling ved åbning af indhold — %s - Angiv som miniaturebillede for spilleliste + Hvad:\\nForespørgsel:\\nIndholdssprog:\\nIndholdsland:\\nAppsprog:\\nTjeneste:\\nGMT-tid:\\nPakke:\\nVersion:\\nOS-version: + Standardhandling når indhold åbnes – %s + Anvend som playlistens miniature Bogmærk spilleliste Fjern bogmærke Føjet til spillelisten Miniaturebillede for spilleliste ændret. - Ændr undertekststørrelse og baggrundsstil. Kræver genstart af appen for at træde i kraft. + Ændr undertekststørrelse og baggrundsstil. Kræver genstart af appen for at træde i kraft Monitorering for hukommelseslækager kan få appen til ikke at svare under heap dumping Rapporter out-of-lifecycle-fejl Importer @@ -348,7 +349,11 @@ \n \n1. Gå til denne webadresse: %1$s \n2. Log ind når du bliver bedt om det -\n3. En download bør starte (det er eksportfilen)
+\n3. Klik på \"Alle Youtube-data medtages\" og fravælg alt bortset fra \"abonnementer\". +\n4. Klik på \"Næste trin\" og derefter \"Opret eksport\". +\n5. Klik på \"Download\" knappen efter den popper frem. +\n6. Klik på \"IMPORTER FIL\" nederst på denne side og vælg den downloadede .zip fil. +\n7. [Såfremt .zip-importeringen slår fejl] Uddrag .csv filen (som normalt findes i \"YouTube og YouTube Music/abonnementer/abonnementer.csv\"). Klik på \"IMPORTER FIL\" nederst på denne side, og vælg den uddragede .csv fil
ditID, soundcloud.com/ditID Bemærk at denne operation kan kræve meget netværkstrafik. \n @@ -364,37 +369,288 @@ Ingen Minimer til baggrundsafspiller Minimer til pop op-afspiller - NewPipe-opdatering tilgængelig! + En NewPipe-opdatering er tilgængelig! sat på pause sat i kø Maksimalt antal genforsøg Maksimalt antal forsøg før downloaden opgives - Sæt på pause ved skift til mobildata - Downloads som ikke kan sættes på pause vil blive genstartet - Kun HTTPS URL-er understøttet + Afbryd på forbrugsafregnede netværk + Nyttigt ved skift til mobildata, selv om nogle downloads ikke kan sættes på pause + Kun HTTPS adresser understøttes Instansen findes allerede - Kunde ikke bekræfte instans - Skriv ind instans-URL + Kunne ikke validere instansen + Skriv instansens adresse Tilføj instans - Finn instanserne du liger på %s - Vælg dine favorit-PeerTube-instanser + Find de instanserne du kan lide på %s + Vælg dine yndlings PeerTube-instanser PeerTube-instanser - Automatisk afspilning - Tøm data + Afspil automatisk + Ryd data Positioner i lister Genopret forrige afspilningsposition Fortsæt afspilning - Skru av for at skjule kommentarer + Slå fra for at skjule kommentarer Vis kommentarer Ingenting - Gentagelse + Gentag Femte handlingstast Fjerde handlingstast Første handlingstast - Andre handlingstast + Anden handlingstast Tredje handlingstast Viser resultater for: %s Åben med LeakCanary er ikke tilgængelig Markér som set + Beskrivelse + Kapitler + Notifikationer + Hjælp + Kunstnere + Luk + Radio + Kategori + Privatliv + Procent + Ny + Systemstandard + Album + Sange + Videoer + Vis ikke + Bland + Vis beskrivelse + Åbn hjemmeside + Sprog + Lav kvalitet (mindre) + Start afspilning automatisk — %s + Aldrig + Kun på Wi-Fi + + %d sekund + %d sekunder + + + %1$s download slettet + %1$s downloads slettet + + Slet alle downloadede filer fra drevet\? + Sæt downloads på pause + Start hovedafspilleren i fuldskærmstilstand + Downloadmappe endnu ikke valgt. Vælg standardmappen nu + Læg automatisk i kø + Konfigurer det spillende streams notifikation + Vis aldersbegrænset indhold (f.eks. 18+) + Slå YouTube \"begrænset tilstand\" til + YouTube har en \"begrænset tilstand\" der skjuler videoer som potientielt er skadelige for børn + Denne video er aldersbegrænset. +\n +\nSlå \"%1$s\" fra i indstillingerne hvis du vil se den. + Nye streams + Notifikationer om nye streams fra abonnementer + reCAPTCHA cookies er ryddet + Slet alle playback positioner\? + Filen er flyttet eller slettet + NewPipe stødte ind i en fejl, tryk for at rapportere + Rapporter på GitHub + Høj kvalitet (større) + Begræns downloadkøen + Ryd de cookies som NewPipe opbevarer når du løser en reCAPTCHA + Farvelæg notifikationen + Afspillernotifikation + En fejl opstod, se notifikationen + Slå fra for at skjule videobeskrivelsen og yderligere information + Slå fra for at gemme metainformationskasser med yderligere information om streammets skaber, streammets indhold eller en søgeforespørgsel + + Download fuldført + %s downloads fuldført + + Lav indlæsningsintervallets størrelse, (som nu ligger på %s) om. En højere værdi kan øge videoindlæsningshastigheden. Ændringer af værdien kræver genstart. + Den aktive spilleliste bliver udskiftet + At skifte fra en afspiller til en anden kan udskifte din kø + Vis metainformation + Lokale søgeforslag + Fjerne søgeforslag + Start ikke videoer i miniafspilleren, men gå direkte til fuldskærmstilstand, hvis automatisk rotering er låst. Du kan stadig se miniafspilleren, hvis du går ud af fuldskærmstilstand + Kunne ikke genkende addressen. Vil du åbne den i en anden app\? + Videohashfunktion notifikation + Notifikationer om videohashfunktioners status + Fejlrapport-notifikation + Notifikationer for at rapportere fejl + Slet playback positioner + Sletter alle playback positioner + Spørg hvor filen skal downloades + Et download ad gangen + Slet downloadede filer + Vil du rydde din download historik eller slette alle downloadede filer\? + Kan ikke gendanne dette download + Ryd download historik + NewPipe projektet tager dit privatliv seriøst. Derfor samler appen intet data uden dit samtykke. +\nNewPipes fortrolighedspolitik forklarer i detaljer, hvilke data der bliver sendt og opbevaret når du sender en nedbrudsrapport. + Kopier en formatteret rapport + Giv tilladelse til at vise over andre apps + Vis playback positionsvisere i lister + Playback positioner slettet + Ryd reCAPTCHA cookies + Der er en afventende download med dette navn + Start downloads + Skaler miniaturebilledet til 1:1 format + Skaler notifikationsminiaturebillederne fra 16:9 til 1:1 format (dette kan medføre forvrængninger) + Rediger hver eneste varselshandling nedenunder ved at trykke på dem. Vælg op til tre af dem som bliver vist i den lille notifikation, via kasserne til højre + Du kan kun vælge op til tre handlinger som kan vises i den lille notifikation! + Buffer + Få Android til at vælge notifikationens farve ud fra den primære farve i miniaturebilledet (virker ikke på alle enheder) + Nattetema + Frem- og tilbagesøgningstid + Denne video er aldersbegrænset. +\nPga. YouTubes politik om aldersbegrænsede videoer har NewPipe ikke adgang til videoen. + Crash afspilleren + Spørg om bekræftelse før du tømmer en kø + Forhåndsvisning af miniaturebilleder på statuslinjen + Sæt i kø som næste + Er sat som næste i køen + Download er begyndt + Vis miniaturebilleder på både låseskærmen og notifikationer + Nylige + Notifikationer er slået fra + Kommentarer + Relaterede objekter + Stryg på elementer for at fjerne dem + Vælg en playliste + Ingen playliste bogmærker endnu + Sproget ændres når appen genstarter + Spillekø + Vis kanalens detaljer + Sæt i kø + Sat i kø + Loader streamets detaljer… + Processere... Det kan tage et øjeblik + Vis hukommelsestab + Deaktiver medietunneler + Vis billedindikatorer + Netværkskrav + Alle netværk + Kontrolfrekvens + Notifikationer ved nye streams + Notifikationer om ny streams fra abonnomenter + Tjek manuelt efter opdateringer + Tjekker efter opdateringer… + Gendanner + \"Hurtig feed\"-tilstand viser ikke mere information om dette. + Tjek efter opdateringer + Fjern sete videoer\? + Deaktiver medietunneler hvis du oplever en sort skærm eller hak ved videoafspilning + Venligst tjek om der allerede eksisterer en problemrapport som diskuterer dit crash. Hvis du opretter duplikatrapporter, tager du tid fra os som vi kunne bruge på at fikse fejlen. + Tjek efter nye streams + Lav en fejlnotifikation + Lokale + Udgiverens bruger er blevet slettet. +\nNewpipe kan ikke indlæse dette feed i fremtiden. +\nVil du fjerne dit abonnement på denne kanal\? + Feedet blev sidst opdateret for %s + Ikke indlæst: %d + Indlæser feed… + Nye feed elementer + Tid siden sidste opdatering for at et abonnoment bliver forældet - %s + Altid opdater + Vælg abonnementer + Vis sete elementer + Dette indhold er ikke tilgængeligt i dit land + Af %s + Videoer på playlisten som allerede er blevet set fjernes. +\nDette kan ikke fortrydes! + Vis miniaturebillede + Tags + Aldersbegrænsning + Dette indhold er ikke understøttet af NewPipe. +\n +\nVi håber at kunne understøtte det i en fremtiden. + Dette indhold er kun tilgængeligt for brugere som har betalt for det. Det kan ikke blive streamet eller downloadet af NewPipe. + Bruger slettet + Dette indhold er privat, så det jan ikke blive streamet eller downloadet af NewPipe. + Nyligt tilføjede + Fremhævede + %s giver denne grund: + + %s lytter + %s lyttere + + 100+ videoer + Udregner hash + Løs + Ingen abonnementer valgt + Vil du slette denne gruppe\? + Licens + + %s nyt stream + %s nye streams + + Semitone + + %d time + %d timer + + + %d dag + %d dage + + Lavet af %s + Slå hurtigtilstand fra + Slå hurtigtilstand til + Hent fra det dedikerede feed når det er muligt + Feed opdateringsgrænse + Feed + + %d valgt + %d valgte + + Processerer feed… + Kanalgrupper + + %d minut + %d minutter + + Fjern sete + Vælg en instans + Forbindelse afbrudt + Fremskridt tabt fordi filen blev slettet + NewPipe blev lukket under arbejde på filen + Kan ikke overskrive filen + For at være i overenstemmelse med GDPR fanger vi din opmærksomhed hentil NewPipes privatpolitik. Venligst læs den med omhu. +\nDu skal acceptere den for at sende os en fejlrapport. + Aflænk (kan skabe forvrængning) + Importer en SoundCloud profil ved at skrive enten dit URL eller ID: +\n +\n1. Slå \"desktop-version\" til i mobilbrowsere. +\n2. Gå til denne adresse: %1$s +\n3. Log ind når du bliver spurgt +\n4. Kopier adressen på den profil du bliver henstillet til. + Vis den oprindelige tidsforskel på elementer + Autogenereret (ingen uploader fundet) + Slå lyd til + Sæt på lydløs + Mest likede + Kunne ikke indlæse kommentarer + Standard Kiosk + Færdig + Tryk på \"Færdig\" når den er løst + Ingen kommentarer + ∞ videoer + Ingen lyttere + + %s seer + %s seere + + Ingen seere + Skift service, nuværende valg: + Kommentarer er slået fra + Ingen apps på din enhed kan åbne dette + Ingen ledig plads på enheden + App sprog + Ja, og delvist sete videoer + Fejl ved indlæsning af feed + Kunne ikke indlæse feed for \'%s\'. + Vis \"crash afspilleren\" + Crash appen + Vis et crash alternativ når afspilleren er i brug \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1b47fba2f..b91fec84a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,7 +1,7 @@ Veröffentlicht am %1$s - Kein Stream-Player gefunden. Möchtest du VLC installieren\? + Kein Stream-Player gefunden. Möchtest du den VLC installieren\? Installieren Abbrechen Im Browser öffnen @@ -373,7 +373,7 @@ Nachbearbeitung In Wiedergabe einreihen System verweigert den Zugriff - Herunterladen fehlgeschlagen + Download fehlgeschlagen Eindeutigen Namen erzeugen Überschreiben Eine Datei mit diesem Namen existiert bereits @@ -684,12 +684,9 @@ \nBitte installiere einen Dateimanager oder versuche, \'%s\' in den Downloadeinstellungen zu deaktivieren. Es wurde kein geeigneter Dateimanager für diese Aktion gefunden. \nBitte installiere einen Storage Access Framework kompatiblen Dateimanager. - Wird bereits im Hintergrund abgespielt Angehefteter Kommentar LeakCanary ist nicht verfügbar - Tonhöhe nach musikalischen Halbtönen anpassen Ändern der Größe des Ladeintervalls (derzeit %s). Ein niedrigerer Wert kann das anfängliche Laden des Videos beschleunigen. Änderungen erfordern einen Neustart des Players. - Geschwindigkeitsstufe ExoPlayer Standard Benachrichtigungen Benachrichtigen über neue abonnierbare Streams @@ -710,5 +707,15 @@ Alle heruntergeladenen Dateien von der Festplatte löschen\? Du hast jetzt diesen Kanal abonniert Alle umschalten - Aktualisierungsintervall + Prüfintervall + Prozent + Halbton + Keine Videostreams für externe Player verfügbar + Qualität für externe Player auswählen + Unbekanntes Format + Keine Audiostreams für externe Player verfügbar + Unbekannte Qualität + Streams, die noch nicht vom Downloader unterstützt werden, werden nicht angezeigt + Der ausgewählte Stream wird von externen Playern nicht unterstützt + Größe des Ladeintervalls für die Wiedergabe \ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index b54b34d60..06c7e4ae4 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -682,11 +682,8 @@ \nΕγκαταστήστε έναν συμβατό με το Πλαίσιο Πρόσβασης Αποθήκευσης.
Το NewPipe παρουσίασε ένα σφάλμα. Πατήστε για αναφορά Εμφάνιση μιας snackbar σφάλματος - Αναπαράγεται ήδη στο παρασκήνιο Καρφιτσωμένο σχόλιο Το LeakCanary δεν είναι διαθέσιμο - Προσαρμόστε τον τόνο με βάση τα μουσικά ημιτόνια - Βήμα τέμπο Εξ\' ορισμού ExoPlayer Αλλάξτε το μέγεθος του διαστήματος φόρτωσης (επί του παρόντος είναι %s). Μια χαμηλότερη τιμή μπορεί να επιταχύνει την αρχική φόρτωση βίντεο. Οι αλλαγές απαιτούν επανεκκίνηση της εφαρμογής. Ειδοποιήσεις @@ -711,4 +708,13 @@ Λάβετε ειδοποίηση Έχετε εγγραφεί τώρα σε αυτό το κανάλι Εναλλαγή όλων + Τοις εκατό + Ημιτόνιο + Οι ροές που δεν υποστηρίζονται ακόμα από τον λήπτη δεν εμφανίζονται + Η επιλεγμένη ροή δεν υποστηρίζεται από εξωτερικούς αναπαραγωγούς + Δεν διατίθενται ροές ήχου για εξωτερικούς αναπαραγωγούς + Δεν διατίθενται ροές βίντεο για εξωτερικούς αναπαραγωγούς + Επιλογή ποιότητας εξωτερικών αναπαραγωγών + Άγνωστος τύπος αρχείου + Άγνωστη ποιότητα \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 666c588ab..c31cb7ffa 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -292,7 +292,7 @@ Quitar todos los datos guardados de páginas web Se vació la caché de metadatos Controles de velocidad de reproducción - Tiempo + Tempo Tono Desvincular (puede causar distorsión) No hay streams disponibles para descargar @@ -558,7 +558,7 @@ Almacenar en memoria (búfer) Repetir ¡Puedes seleccionar como máximo tres acciones para mostrar en la notificación compacta! - Edita cada acción de la notificación pulsando sobre ella. Selecciona hasta tres de ellas para mostrarlas en la notificación compacta usando las casillas de verificación a la derecha + Edita cada acción de notificación debajo pulsando sobre ella. Selecciona hasta tres de ellas para que aparezcan en la notificación compacta usando las casillas de verificación a la derecha Botón de quinta acción Botón de cuarta acción Botón de tercera acción @@ -686,16 +686,40 @@ No se encontró ningún gestor de archivos adecuado para esta acción. \nPor favor instale un gestor de archivos compatible con \"Sistema de Acceso al Almacenamiento\". Comentario fijado - Ya se reproduce en segundo plano LeakCanary no está disponible ExoPlayer valor por defecto - Paso de tempo Cambia el tamaño del intervalo de carga (actualmente %s). Un valor más bajo puede acelerar la carga inicial del vídeo. Los cambios requieren un reinicio del reproductor. - Ajustar el tono por semitonos musicales Notificaciones Nuevos streams Notificación del reproductor Configurar notificación de la reproducción en curso + Ahora estás suscrito a este canal + Cualquier red + , + Comprobar la existencia de nuevos directos + Notificaciones de nuevos directos + Notificar de nuevos directos desde las suscripciones + Frecuencia de comprobación + ¿Desea borrar del disco todos los archivos descargados\? + Las notificaciones están desactivadas + Recibir notificaciones + Conmutar todo + Cargando detalles del directo… + Notificaciones sobre nuevos directos para suscriptores + Se requiere conexión a red + + %s nuevo directo + %s nuevos directos + + Porcentaje + Semitono + No se muestran flujos cuya descarga aún no está soportada + El flujo seleccionado no es soportado por reproductores externos + No hay flujos de audio disponibles para reproductores externos + No hay flujos de video disponibles para reproductores externos + Elija la calidad para reproductores externos + Formato desconocido + Calidad desconocida SponsorBlock Ver Sitio Web diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index d04834f8a..7cb96405a 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -2,8 +2,8 @@ Alustamiseks toksa suurendusklaasi ikooni. Avaldatud %1$s - Voogesituseks puudub pleier. Kas paigaldada VLC? - Voogesituseks puudub pleier (selleks võib paigaldada VLC). + Voogesituseks puudub pleier. Kas paigaldame VLC\? + Voogesituseks puudub pleier (selleks võid paigaldada VLC). Paigalda Tühista Ava veebilehitsejas @@ -305,7 +305,7 @@ reCAPTCHA nõue reCAPTCHA nõude taotlus © %1$s %2$s %3$s alla - Vaba kergekaaluline Androidi voogesitus. + Vaba ja lihtne voogesitus Androidis. Kui sul on ideid kujunduse muutmisest, koodi puhastamisest või suurtest koodi muudatustest - abi on alati teretulnud. Mida rohkem tehtud, seda paremaks läheb! NewPipe arendajad on vabatahtlikud, kes kulutavad oma vaba aega, toomaks sulle parimat kasutamise kogemust. On aeg anda tagasi aidates arendajaid ja muuta NewPipe veel paremaks, nautides ise tassi kohvi. Anneta @@ -674,7 +674,6 @@ NewPipe töös tekkis viga, sellest teavitamiseks klõpsi Jooksuta meediamängija kokku Näita veateate akent - Meedia esitamine taustal toimib juba Teavitus vigadest Teavitused vigadest informeerimiseks Tekkis viga, vaata vastavat teadet @@ -709,6 +708,13 @@ Sa oled nüüd selle kanali tellija , Lülita kõik sisse - Reguleeri helikõrgust muusikaliste pooltoonide kaupa - Tempo samm + Välise pleieri jaoks ei leidu sobilikke helivoogusid + Need meediavood, mida allalaadija ei oska kasutada, on peidetud + Välise pleieri jaoks ei leidu sobilikke videovoogusid + Vali välis pleieri jaoks sobilik kvaliteet + Tundmatu vorming + Teadmata kvaliteet + Valitud meediavood ei ole toetatud välises pleieris + Protsent + Pooltoon \ No newline at end of file diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 1c710641e..80e2d5a04 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -666,7 +666,6 @@ Gehitu bideo hau isatsari Erakutsi \"Itxi erreproduzigailua\" Prozesatzen... Itxoin mesedez - Atzeko planoan erreproduzitzen dagoeneko Erroreen txostenen jakinarazpena Jakinarazpenak erroreen berri emateko NewPipe-k errore bat aurkitu du, sakatu berri emateko @@ -695,8 +694,6 @@ Kanal honetara harpidetu zara , Txandakatu denak - Doitu tonua semitono musikalen arabera - Tempo urratsa Aldatu karga maiztasun tamaina (unean %s). Balio txikiago batek bideoaren hasierako karga azkartu dezake. Erreproduzigailuaren berrabiarazte bat behar du. Harpidetzen jario berriei buruz jakinarazi Ezabatu deskargatutako fitxategi guztiak biltegitik\? @@ -711,4 +708,6 @@ Jakinarazi ExoPlayer lehenetsia Beharrezko sare konexioa + Portzentaia + Semitonoa \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index d0f4d3d84..a00c64ad0 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -683,10 +683,7 @@ نیوپایپ به خطایی برخورد. برای گزارش، بزنید خطایی رخ داد. آگاهی را ببینید نظر سنجاق شده - در حال پخش در پس‌زمینه لیک‌کاناری موجود نیست - گام سرعت - تنظیم زیر و بم با شبه‌تن‌ها تغییر اندازهٔ بازهٔ بار (هم‌اکنون %s). مقداری پایین‌تر، می‌تواند بار کردن نخستین ویدیو را سرعت بخشد. تغییرها نیاز به یک آغاز دوبارهٔ پخش‌کننده دارند. پیش‌گزیدهٔ اگزوپلیر آگاهی‌ها @@ -711,4 +708,13 @@ پاک کردن تمامی پرونده‌های بارگرفته از دیسک؟ هر شبکه‌ای اکنون مشترک این کانال شده‌اید + نیم‌پرده + درصد + هیچ جریان ویدیویی‌ای برای پخش‌کننده‌های خارجی موجود نیست + جریان‌هایی که هنوز به دست بارگیر پشتیبانی نمی‌شوند نشان داده نشده‌اند + هیچ جریان صوتی‌ای برای پخش‌کننده‌های خارجی موجود نیست + جریان گزیده به دست پخش‌کننده‌های خارجی پشتیبانی نمی‌شود + گزینش کیفیت برای پخش‌کننده‌های خارجی + قالب ناشناخته + کیفیت ناشناخته \ No newline at end of file diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index b4e807e5e..8eef663b0 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -684,4 +684,5 @@ Näytä soitinta käytettäessä soittimen kaatamisen vaihtoehto Näytä virheen ponnahdusilmoitus Uudet syötteet + Ilmoitukset \ No newline at end of file diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index 1795fc98c..d8c6682d6 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -1,6 +1,6 @@ - Pindutin ang magnifying glass para makapagsimula. + Pindutin ang magnifying glass upang magsimula. Inilathala noong %1$s Walang nakitang stream player. I-install ang VLC\? Walang nakitang stream player (pwede mong i-install ang VLC para ma-play ito). @@ -13,7 +13,7 @@ I-download ang stream file Maghanap Ayos ng App - Ibig mo bang sabihin \"%1$s\"\? + \"%1$s\" ba ang tinutukoy mo\? Ibahagi sa Gumamit ng ibang video player Natatanggal ang tunog sa ilang mga resolusyon @@ -42,7 +42,7 @@ Ilalagay rito ang mga na-download na video file Download folder ng mga audio Ipinapakita ang mga resulta para sa: %s - Buksan gamit ng + Buksan gamit ang Pang-apat na action button Ipakita ang \"I-play gamit Kodi\" Pangalawang action button @@ -58,7 +58,7 @@ Tema Lokal na mungkahi Remote na mungkahi - Markahang napanood + Markahan bilang napanood Kusang ipila Ulitin Halo-halo @@ -116,6 +116,145 @@ Gamitin ang mabilis ngunit di-saktong seek Haba ng fast forward/-rewind seek Ituloy ang pagpapalabas - Mga Patok Ngayon + Patok Ngayon Subaybayan ang mga napanood nang video + Kumakailan + Kategorya + Mga Tag + Praybasi + Nakapatay ang Mga Notipikasyon + Publiko + Mga Puna + Nagda-dawnload ang NewPipe + Naka-bukas + Wika ng App + Tungkol dito + Ang video na ito ay may paghihigpit sa edad. +\nDahil sa mga bagong polisiya ng Youtube, hindi maaring ma-access ng NewPipe ang mga video streams nito, kaya hindi ito maipapalabas. + Notipikasyon sa NewPipe + + %s nakikinig + Mga %s nakikinig + + 100+ na mga video + ∞ na mga video + Walang Komento + Ipalabas Lahat + Mga Notipikasyon + Pinagbabawal ang pagkomento + Mga pinahihintulutang karakter sa pangalan ng file + Mga Lisensya + Kasaysayan + Laman ng pangunahing pahina + Live + Palagi + Gusto mo bang burahin ito sa kasaysayan ng paghanap\? + Laman + + %d oras + mga %d oras + + Karaniwan ng Sistema + Nakapribado ang content na ito, kaya hindi ito maipalabas o mai-download ng NewPipe. + Sinisimulan na ang Pagdownload + Pinusuan ng creator + Ni %s + Tulong + Naka-pin na komento + Buksan ang website + Lisensya + Wika + Pribado + URL ng Thumbnail + Hindi nakalista + Nakapatay + Bago at patok + Panimulang wika ng content + Itsura ng App + + %d segundo + Mga %d segundo + + Nilikha ni %s + Mga Kabanata + Walang app sa device mo ang makakabukas nito + Tampok + Huling Pinanood + Kasaysayan + Walang nakikinig + Walang nahanap + Pumalit sa Likuran + Linisin ang kasaysayan ng panonood + + %s nanonood + Mga %s nanonood + + Walang mga video + Madalas na Pinanood + + %s video + Mga %s na video + + + %d minuto + Mga %d minuto + + + %d araw + mga %d araw + + Buksan ang pangunahing player sa fullscreen + Itakda ang kasalukuyang notipikasyon ng playing stream + Mga download + Ulat sa problema + Mga channel + Mga listahan ng nilalaman + Mga bidyo + Mga pangyayari + Mga album + Naka-disable + Alisin + Pinakamainam na resolusyon + Natanggal ang file + Isang Beses Lang + Mga HTTPS URL lang ang suportado + Mga kanta + Awtopaandar + Mahahanap mo sa %s ang hilig mong mga instansya + Patuloy na mag-play pagkatapos ng istorbo (hal. tawag sa telepono) + I-download + Huwag buksan ang mga video sa mini player at dumiretso na sa fullscreen mode kung naka-lock ang awto rotasyon. Magagamit mo pa rin ang mini player kung aalis ka sa fullscreen + Ipakita ang tip ng \"I-hold para ipila\" + Ipakita ang tip tuwing pinpindot ang background o ang buttong pumapop-up sa video na \"Details:\" + Hindi makilala ang URL. Buksan sa ibang app\? + Mga instansya ng PeerTube + Ang video na ito ay may paghihigpit sa edad. +\n +\nBuksan ang \"%1$s\" sa ayos ng app kung gusto mong makita ito. + Magdagdag ng mga instansya + Hindi maberipika ang instansya + Naidagdag na ang instansyang iyan + Ugali + Bidyo at tunog + Kasaysayan at cache + Mga update + Ipakita ang nilalamang hindi pambata + Ipakita ang nilalamang maaaring makahamak sa bata dahil limitado ito sa edad (hal. 18+) + May \"Restricted Mode\" ang YouTube kung saan nakatago ang nilalamang hindi pambata + Notipikasyon sa pag-update ng app + Mga notipikasyon para sa player ng NewPipe + Mga artista + Gamitin ang \"Restricted Mode\" ng YouTube + Nagpe-play sa popup mode + Nagpe-play sa background + Player + Ilagay ang URL ng instansya + Piliin ang iyong mga paboritong instansya ng PeerTube + Mga download + Lahat + Default na bansa ng nilalaman + Di-suportadong URL + Notipikasyon ng player + Mga track + Mga gumagamit \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4a4559477..327877651 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -84,7 +84,7 @@ Tout Défi reCAPTCHA Défi reCAPTCHA demandé - Ouvrir en mode pop-up + Ouvrir en mode flottant Lecture en mode flottant Désactivés Quoi :\\nRequest :\\nContent Language :\\nContent Country :\\nApp Language :\\nService :\\nGMT Time :\\nPackage :\\nVersion :\\nOS version : @@ -326,7 +326,7 @@ Morceaux Utilisateurs Accélérer pendant les silences - Étape + Graduation Réinitialiser Minimiser lors du changement d’application Action lors du basculement à une autre application depuis le lecteur vidéo principal — %s @@ -551,7 +551,7 @@ Impossible de reconnaitre l’URL fournie. Voulez-vous l’ouvrir avec une autre application \? Ajouter automatiquement à la liste de lecture La liste de lecture du lecteur actif sera remplacée - Confirmer av. de suppr. la liste de lecture + Confirmer avant de supprimer la liste de lecture Rien Chargement Lire aléatoirement @@ -685,11 +685,8 @@ Aucun gestionnaire de fichier approprié n\'a été trouvé pour cette action. \nVeuillez installer un gestionnaire de fichiers ou essayez de désactiver \'%s\' dans les paramètres de téléchargement. Commentaire épinglé - Une lecture est déjà en arrière-plan LeakCanary n\'est pas disponible Modifie la taille de l\'intervalle de chargement (actuellement %s). Une valeur plus faible peut accélérer le chargement initial des vidéos . - Règler la hauteur par demi-tons musicaux - Pas du tempo Valeur par défaut d’ExoPlayer Nouveaux flux Configurer la notification du flux en cours de lecture @@ -702,7 +699,7 @@ Connexion réseau requise Notifications Notifications désactivées - Vous êtes maintenant abonné(e) à cette chaîne + Vous vous êtes maintenant abonné à cette chaîne Notification du Lecteur Notifications pour de nouveaux flux des abonnements Supprimer tous les fichiers téléchargés du disque \? @@ -712,5 +709,15 @@ Fréquence de vérification Notifications pour de nouveaux flux des abonnements , - Sélectionner/Désélectionner tout + Tout basculer + Pourcent + Demi-ton + Les flux qui ne sont pas encore supportés ne sont pas montrés + Aucun flux audio n\'est disponible pour les lecteurs externes + Sélectionner la qualité pour les lecteurs externes + Format inconnu + Qualité inconnue + Le flux séléctionné n\'est pas supporté par les lecteurs externes + Aucun flux vidéo n\'est disponible pour les lecteurs externes + Taille de l\'intervalle de chargement de la lecture \ No newline at end of file diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 52225b843..06b35acd3 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -61,7 +61,7 @@ Os metadatos da caché foron eliminados Eliminar todos os datos de páxinas en caché Os metadatos da caché foron eliminados - Colocar a seguinte emisión na cola automaticamente + Colocar a seguinte emisión na fila automaticamente Continúa rematando (non se repite) a cola de reprodución engadindo unha transmisión relacionada Suxestións de procura Escolla suxestións a mostrar ao procurar @@ -226,7 +226,7 @@ \nA política de privacidade do NewPipe explica con máis detalle que datos son enviados e gardados cando envía un relatorio de erros.
Ler a política de privacidade Licenza do NewPipe - NewPipe é un software libre copyleft: Pode usar, estudar compartir e melloralo a vontade. En concreto, pode redistribuír e / ou modificala segundo os termos da Licenza Pública Xeral GNU publicada pola Free Software Foundation, xa sexa a versión 3 da licenza, ou (na súa opción) calquera outra versión posterior. + NewPipe é un software libre copyleft: Pode usar, estudar compartillar e melloralo a vontade. En concreto, pode redistribuír e / ou modificala segundo os termos da Licenza Pública Xeral GNU publicada pola Free Software Foundation, xa sexa a versión 3 da licenza, ou (na súa opción) calquera outra versión posterior. Ler a licenza Historial Historial @@ -321,8 +321,8 @@ Avanzar rápido durante os momentos de silencio Paso Reiniciar - Para cumprirmos co Regulamento Xeral Europeo de Protección de Datos (GDPR), chamamos a súa atención sobre a nova política de privacidade do NewPipe. Por favor, léao con coidado. -\nDebe aceptalo para nos enviar un relatorio de erro. + Para cumprirmos co Regulamento Xeral Europeo de Protección de Datos (GDPR), chamamos a súa atención sobre a nova política de privacidade do NewPipe. Por favor, léaa con coidado. +\nDebe aceptala para nos enviar un relatorio de erro. Aceptar Recusar Sen límite @@ -434,7 +434,7 @@ Non se puido atopar o servidor Non se puido establecer unha conexión segura Non se pode crear o cartafol de destino - Non se puido crear este ficheiro + Non se pode crear o ficheiro Amosar o erro Hai unha descarga pendente con este nome Hai unha descarga en curso con este nome @@ -557,7 +557,7 @@ Privado Público URL da miniatura - Soporte + Apoio Idioma Límite de idade Privacidade @@ -672,8 +672,6 @@ Enfileirado Procurar actualizacións Procurar manualmente novas versións - Axustar o ton do semitóns musicais - Paso do tempo A procurar actualizacións… A partir do Android 10, só o \'Sistema de Acceso ao Almacenamento\' está soportado Cambia o tamaño do intervalo de carga (actualmente %s). Un valor menor pode acelerar o carregamento do vídeo. Cambios poden precisar un reinicio do reprodutor. @@ -688,4 +686,28 @@ Amosa unha opción de travamento ao usar o reprodutor Travar o reprodutor LeakCanary non está dispoñíbel + Notificacións sobre novas emisións para subscricións + Frecuencia de verificación + Conexión á rede necesaria + , + Alternar todo + + %s nova emisión + %s novas emisións + + Por cento + Semitón + Notificar sobre novas emisións de subscricións + Notificacións sobre novas emisións + Notificación do reprodutor + Novas emisións + Carregando detalles da emisión… + Verifique se hai novas emisións + Configurar a notificación da emisión actual + Notificacións + Calquera rede + Desexa eliminar todos os ficheiros descarregados do disco\? + As notificacións están desactivadas + Recibir notificacións + Agora está subscrito a esta canle \ No newline at end of file diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index a0e6c32a9..b0ece06f1 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -706,11 +706,8 @@ התראת דיווח שגיאה לא נמצאו מנהלי קבצים שמתאימים לפעולה הזאת. \nנא להתקין מנהל קבצים שתומך בתשתית גישה לאחסון. - כבר מתנגן ברקע הערה ננעצה LeakCanary אינה זמינה - התאמת גובה הצליל לפי חצאי טונים מוזיקליים - צעד מקצב ברירת מחדל של ExoPlayer שינוי גודל מרווח הטעינה (כרגע %s). ערך נמוך יותר עשוי להאיץ את טעינת הווידאו הראשונית. שינויים דורשים את הפעלת הנגן מחדש. התראות על תזרימים חדשים להרשמה @@ -737,4 +734,13 @@ למחוק את כל הקבצים שהורדו מהכונן\? התראות מושבתות נרשמת לערוץ הזה + אחוז + חצי טון + תזרימים שעדיין לא נתמכים על ידי המוריד לא מופיעים + התזרים הנבחר לא נתמך על ידי נגנים חיצוניים + אין תזרימי שמע שזמינים לנגנים חיצוניים + איכות לא מוכרת + אין תזרימי וידאו שזמינים לנגנים חיצוניים + בחירת איכות לנגנים חיצוניים + תצורה לא מוכרת \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 01cbab7f6..0ca0c75dc 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -340,7 +340,7 @@ NewPipe razvijaju volonteri koji provode vrijeme donoseći vam najbolje iskustvo. Vratite im kako biste programerima učinili da NewPipe bude još bolji dok uživate u šalici kave. Koje su kartice prikazane na glavnoj stranici Konferencije - Preferirana \'otvori\' akcija + Željena radnja otvaranja streama Zadana radnja pri otvaranju sadržaja — %s Titlovi Promijeni veličinu podnaslova reproduktora i pozadinske stilove reproduktora. Za stupanje na snagu, program se mora ponovo pokrenuti @@ -602,8 +602,8 @@ Ovaj je video dobno ograničen. \nZbog novih YouTube pravila za videa s dobnim ograničenjem, NewPipe ne može pristupiti nijednoj vlastitoj video emisiji i stoga je ne može reproducirati. Preuzimanje je započeto - Dolje možeš odabrati omiljenu noćnu temu - Odaberi omiljenu noćnu temu – %s + Dolje možete odabrati željenu noćnu temu + Odaberi željenu noćnu temu – %s Automatski (tema uređaja) Radio Istaknuto @@ -687,4 +687,29 @@ NewPipe je naišao na grešku, dodirni za prijavu Došlo je do greške, pogledaj obavijest Prekini rad playera + Obavijest reproduktora + Prilagođavanje obavijesti reproduktora + Obavijesti + Novi videozapisi + Obavijesti novih streamova pretplaćenih kanala + Želite li izbrisati sve preuzete datoteke\? + Obavijesti su onemogućene + Pretplatili ste se ovome kanalu + , + Uključiti/isključiti sve + Bilo kakva mreža + Obavijesti novih streamova pretplaćenih kanala + Pokaži zalogajnicu greške + Učitavanje pojedinosti streama… + Pokrenite provjeru novih streamova + Učestalost provjere + LeakCanary nije dostupno + Podešavanje visine tona po glazbenim polutonovima + Obavijesti o novim streamovima + Potrebna mrežna veza + Zadano za ExoPlayer + Primite obavijesti + Za ovu radnju nije pronađen odgovarajući upravitelj datoteka. +\nMolimo vas da instalirate upravitelj za datoteke ili da pokušate onemogućiti \'%s\' u postavkama preuzimanja. + Prikvačeni komentar \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index adaf6a8c0..e5457d3e1 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -683,6 +683,5 @@ %1$s letöltés törölve Rögzített megjegyzés - Már megy a lejátszás a háttérben LeakCanary nem elérhető \ No newline at end of file diff --git a/app/src/main/res/values-hy/strings.xml b/app/src/main/res/values-hy/strings.xml index 2a2716d47..10d397aef 100644 --- a/app/src/main/res/values-hy/strings.xml +++ b/app/src/main/res/values-hy/strings.xml @@ -142,4 +142,79 @@ Պատմություն Պատմություն Թրենդային + Նվագացանկեր + [Անհայտ] + Մաքրել որոնման պատմությունը + Մեկնաբանությունները անջատված են + Միշտ հարցնել + Լցնել + Մեծացնել + Գեներացված + Ներմուծել ֆայլ + Ստուգել թարմացումները + Ինքնին + Բարձր որակ (մեծ) + Ցածր որակ (փոքր) + հերթագրված + Ջնջել ներբեռնված ֆայլերը + Սկսել ներբեռնումները + Օգտատերեր + Հեռացնել + Ներմուծում եմ… + Տարիքային սահմանափակում + Անհայտ որակ + Նվագել Kodi֊ով + Լռեցնել + Անսահման + Չի բեռնվել՝ %d + Թարմացվել է՝ %s + Ներբեռնումը սկսվեց + Ռադիո + Ներկել ծանուցումները + Մեկնաբանություններ + Թեժ 50 + Տեսնել նկարագիրը + Որոնման հուշումներ + Մաքրել տվյալները + + %s լսող + %s լսող + + + %s հետևորդ + %s հետևորդ + + Տոկոս + Աղյուսակ + Ներմուծել + Նվագել + Մանրամասներ + Նշել նվագացանկ + Տառեր և թվեր + Բեռնումներ + Եղավ + Նվագել ամենը + Ուղիղ + Շարունակել նվագարկումը + Օգնություն + Լուծել + Նշել ալիք + Ցույց չտալ + Խառը + Հարմարվել + Ափփի լեզու + Ոչինչ + Առանց հետևորդ + Հոսքեր + Ավելացնել նվագացանկին + Անենթագիր + Ենթագրեր + Անջատված + Նկարագիր + Ծանուցումներ + Բացել կայքը + Գլուխներ + Հանրային + Պիտակներ + Ծանուցումները անջատված են \ No newline at end of file diff --git a/app/src/main/res/values-ia/strings.xml b/app/src/main/res/values-ia/strings.xml index e9d400be2..d5bc90e6f 100644 --- a/app/src/main/res/values-ia/strings.xml +++ b/app/src/main/res/values-ia/strings.xml @@ -229,7 +229,6 @@ Aperir con Suggestiones de recerca remote Cargar miniaturas - Notification Monstrante resultatos pro: %s Solmente alicun apparatos pote reproducer videos 2K/4K Initiar le reproductor principal in schermo plen diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 6231a7718..f25594776 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -121,7 +121,7 @@ Terlepas apakah Anda memiliki ide untuk; terjemahan, perubahan desain, pembersihan kode, atau perubahan kode yang signifikan, segala bantuan akan selalu diterima. Semakin banyak akan semakin baik jadinya! Baca lisensi Kontribusi - Berlangganan + Subscribe Disubscribe Apa Yang Baru Lanjutkan pemutaran @@ -260,7 +260,7 @@ Putar otomatis video berikutnya Berhenti berlanggan channel Tidak bisa memperbarui langganan - Langganan + Subscription Gunakan tinjau cepat tak pasti Memungkinkan pengguna memilih posisi waktu video dengan cepat tetapi dengan tingkat presisi yang rendah. Mencari 5, 15 atau 25 detik tidak berhasil dengan ini NewPipe adalah perangkat lunak libre copyleft: Anda bisa menggunakannya, mempelajarinya, berbagi, dan meningkatkannya. Secara khusus Anda bisa mendistribusikan ulang dan/atau memodifikasinya dibawah syarat Lisensi Publik Umum GNU yang diterbitkan oleh Free Software Foundation, baik versi 3 dari Lisensi, atau (sesuai pilihan Anda) versi yang lebih baru. @@ -671,7 +671,6 @@ Tidak ada manajer file yang ditemukan untuk tindakan ini. \nMohon instal sebuah manajer file yang kompatibel dengan Storage Access Framework. Komentar dipin - Sudah diputar di latar belakang LeakCanary tidak tersedia SponsorBlock @@ -722,7 +721,6 @@ Gunakan API ReturnYouTubeDislike untuk menampilkan jumlah dislike untuk video. Ini hanya bekerja untuk video YouTube.\nPERHATIAN: Alamat IP Anda akan terlihat oleh API. Gunakan dengan risiko Anda sendiri! Langkah tempo Default ExoPlayer - Atur nada berdasarkan semitone musik Ubah ukuran interval pemuatan (saat ini %s). Sebuah nilai yang rendah mungkin dapat membuat pemuatan video awal lebih cepat. Membutuhkan sebuah pemulaian ulang pada pemain. Memuat detail stream… Frekuensi pemeriksaan @@ -745,4 +743,14 @@ Notifikasi stream baru Beritahu tentang stream baru dari notifikasi Anda sekarang berlangganan ke channel ini + Semiton + Persen + Stream yang dipilih tidak didukung oleh pemain eksternal + Tidak ada stream video yang tersedia untuk pemain eksternal + Kualitas tidak diketahui + Stream yang belum didukung oleh pengunduh tidak ditampilkan + Tidak ada stream audio yang tersedia untuk pemain eksternal + Pilih kualitas untuk pemain eksternal + Format tidak diketahui + Ukuran interval pemuatan playback \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 9f7b74d8e..f347eba67 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -94,7 +94,7 @@ In sottofondo Popup Risoluzione predefinita lettore popup - Mostra altre risoluzioni + Mostra risoluzioni più elevate Solo alcuni dispositivi possono riprodurre video 2K/4K Formato video predefinito Ricorda proprietà lettore popup @@ -664,7 +664,7 @@ Avvia il lettore principale a schermo intero Aggiunto alla coda come prossimo Accoda come prossimo - Elaborazione... Potrebbe volerci un attimo + Elaborazione… Potrebbe volerci un attimo Controlla aggiornamenti Verifica manualmente la presenza di nuove versioni Controllo aggiornamenti… @@ -683,32 +683,39 @@ Non è stato trovato alcun gestore di file appropriato per questa azione. \nInstallane uno compatibile con Storage Access Framework. Commento in primo piano - Già in riproduzione in sottofondo LeakCanary non è disponibile - Regola il tono secondo i semitoni musicali Predefinito ExoPlayer Cambia la dimensione dell\'intervallo da caricare (attualmente %s). Un valore basso può velocizzare il caricamento iniziale del video. La modifica richiede il riavvio del lettore. - Passo tempo - Notifiche di nuove stream dalle iscrizioni + Notifiche di nuovi contenuti dalle iscrizioni Frequenza controllo - Richiesta connessione alla rete + Connessione di rete richiesta Ricevi le notifiche - Sei ora iscritto a questo canale + Ti sei iscritto a questo canale , Attiva/disattiva tutti - Notifiche per nuove stream - Notifica lettore - Configura la notifica della stream attualmente in riproduzione + Notifiche per nuovi contenuti + Notifica del lettore multimediale + Configura la notifica dell\'elemento attualmente in riproduzione Notifiche - Nuove stream + Nuovi contenuti - %s nuova stream - %s nuove stream + %s nuovo contenuto + %s nuovi contenuti - Caricando i dettagli della stream… - Cancellare tutti i file scaricati dal dispositivo\? - Avvia controllo per nuove stream + Caricamento dei dettagli dei contenuti… + Cancellare dal dispositivo i file scaricati\? + Controlla la presenza di nuovi contenuti Qualsiasi rete Le notifiche sono disabilitate - Notifica se ci sono nuove stream dalle iscrizioni + Notifica la presenza di nuovi contenuti dalle iscrizioni + Semitono + Percentuale + Il flusso selezionato non è supportato dai lettori esterni + Non sono disponibili flussi video per i lettori esterni + I flussi che non sono ancora supportati dal downloader non vengono visualizzati + Non sono disponibili flussi audio per i lettori esterni + Seleziona qualità per lettori esterni + Qualità sconosciuta + Formato sconosciuto + Dimensione dell\'intervallo di caricamento della riproduzione \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index a0699f229..955912126 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -663,7 +663,6 @@ エラーが発生しました。通知をご覧ください NewPipe はエラーに遭遇しました。タップして報告 スナックバーにエラーを表示 - 既にバックグラウンドで再生されています 固定されたコメント この動作に適切なファイルマネージャが見つかりませんでした。 \nStorage Access Frameworkと互換性のあるファイルマネージャをインストールしてください。 @@ -673,7 +672,6 @@ エラー通知を作成 エラーを報告する通知 LeakCanaryが利用不可能です - 緩急音階 プレイヤー通知 ストリームの詳細を読み込んでいます… 登録チャンネルの新しいストリームについて通知する @@ -695,4 +693,8 @@ 通知 現在再生しているストリームの通知を構成 読み込む間隔を変更します (現在 %s)。小さい値にすると初回読み込み時間が短くなります。変更にはプレイヤーの再起動が必要です。 + 必要なネットワークの種類 + パーセント + 半音 + すべてのネットワーク \ No newline at end of file diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/main/res/values-kk/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index c28c80f38..4db63cf13 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -156,8 +156,8 @@ Detaļas: Jūsu komentārs (Angliski): Kas:\\nRequest:\\nContent Valoda:\\nContent Valsts:\\nApp Valoda:\\nService:\\nGMT Laiks:\\nPackage:\\nVersion:\\nOS versija: - Notifikācijas priekš video apstrādes progresa - Video Haša Notifikācija + Notifikācijas video apstrādes progresam + Video haša notifikācija Atcerēties pēdējo popup izmēru un pozīciju Atcerēties popup īpašības Noklusējuma popup izšķirtspēja @@ -223,9 +223,9 @@ Pārslēgt uz Popup Pārslēgt uz Fonu [Nezināms] - Notifikācijas priekš NewPipe versijas - Aplikācijas Atjauninājuma Notifikācija - Notifikācijas priekš NewPipe fona un popup atskaņotājiem + Notifikācijas NewPipe versijām + Aplikācijas atjauninājuma notifikācija + Notifikācijas priekš NewPipe atskaņotāja NewPipe Notifikācija Fails Tikai Vienreiz @@ -253,7 +253,7 @@ Šis video ir vecuma ierobežots. \n \nIeslēdziet \"%1$s\" iestatījumos, ja vēlaties to redzēt. - YouTube piedāvā \"ierobežotu režīmu\", kas slēpj iespējams pieaugušo saturu + YouTube piedāvā \"ierobežotu režīmu\", kas slēpj potenciāli pieaugušo saturu Ieslēgt YouTube \"Ierobežoto režīmu\" Rādīt saturu, iespējams nepiemērotu bērniem, jo tam ir vecuma ierobežojums Rādīt vecuma ierobežotu saturu @@ -273,7 +273,7 @@ Ievadīt instances saites URL Pievienot instanci Atrodiet instances, kas jums patīk ar %s - Izvēlaties jūsu mīļāko PeerTube instanci + Izvēlaties jūsu mīļākās PeerTube instances PeerTube instances Valoda nomainīsies, kad aplikāciju restartēs Neviena lietotne jūsu ierīcē nevar šo atvērt @@ -493,7 +493,7 @@ Rādīt \"Nospiediet, lai pievienotu\" padomu Automātiski atskaņot Lejupielādēt - Turpināt atskaņošanu pēc pārtraukumiem (piemēram telefona zvana) + Turpināt atskaņošanu pēc pārtraukumiem (piemēram, telefona zvana) Turpināt atskaņošanu Saglabājiet skatītos videoklipus Dzēst datus @@ -502,9 +502,9 @@ Saglabāt pēdējo atskaņošanas pozīciju Atsākt atskaņošanu Skatīšanās vēsture - Glabāt meklēšanas vēsturi uz telefona + Glabāt meklēšanas vēsturi atmiņā Meklēšanas vēsture - Izvēlaties, kādus ieteikumus rādīt, rakstot meklēšanas joslā + Izvēlieties, kādus ieteikumus rādīt, rakstot meklēšanas joslā Meklēšanas ieteikumi Velkot ar pirkstu, mainiet video atskaņošanas spilgtumu Spilgtuma kontrole, atskaņojot video @@ -522,15 +522,15 @@ Izslēdziet, ja nevēlaties redzēt video aprakstus un papildus informāciju Rādīt video aprakstu Rādīt \'Nākošos\' un \'Līdzīgos\' videoklipus - Izslēdziet, ja vēlaties neredzēt komentārus + Izslēdziet, lai paslēptu komentārus Rādīt komentārus Izslēdziet, ja vēlaties nelādēt video attēlus, ietaupot datus un atmiņu. Opcija notīra kešatmiņu, izdzēšot visus saglabātos video attēlus Ielādēt video attēlus Tagadējā atskaņošanas rinda tiks aizvietota - Mainoties vienam video uz citu, iespējams notīrīsies jūsu atskaņošanas rinda + Mainoties vienam video uz citu, iespējams, notīrīsies jūsu atskaņošanas rinda Prasīt apstiprinājumu, pirms notīrīt atskaņošanas rindu - Uz priekšu/ uz atpakaļu, meklētāja ilgums - Neprecīzs meklētājs atļauj video atskaņotājam atrast pozīciju atrāk, bet ar zemāku precizitāti. Meklēšana 5, 15 vai 25 sekundes uz priekšu vai atpakaļ, nestrādā ar šo opciju + Uz priekšu/atpakaļ meklētāja ilgums + Neprecīzs meklētājs atļauj video atskaņotājam atrast pozīciju ātrāk, bet ar zemāku precizitāti. Meklēšana 5, 15 vai 25 sekundes uz priekšu vai atpakaļ, nestrādā ar šo opciju Izmantot ātru, neprecīzu meklētāju Melna Tumša @@ -539,7 +539,7 @@ Noklusējuma video formāts Noklusējuma audio formāts Audio - Ļaujiet Android pielāgot paziņojuma krāsu atbilstoši galvenajai krāsai video attēlā (ņemiet vērā, ka tas nav pieejams visās ierīcēs) + Ļaujiet Android pielāgot notifikācijas krāsu atbilstoši galvenajai krāsai video attēlā (ņemiet vērā, ka tas nav pieejams visās ierīcēs) Kopīgot Atvērt ar Atvērt pārlūkprogrammā @@ -548,22 +548,22 @@ Netika atrasts video atskaņotājs (jūs varat instalēt VLC, lai to atskaņotu). Netika atrasts video atskaņotājs. Instalēt VLC\? Publicēts %1$s - Nospiediet \"Meklēt\", lai sāktu. + Nospiediet uz meklēšanas ikonas, lai sāktu. Iekrāsot notifikāciju Nekas - Lādējas + Ielādējas Sajaukt Atkārtot Jūs varat izvēlēties tikai 3 darbības, kuras rādīs kompaktajā notifikācijā! - Rediģējiet katru notifikācijas darbību pieskaroties tai. Izvēlaties trīs darbības, kuras rādīs kompaktā notifikācijā, izmantojot rūtiņas labajā pusē + Rediģējiet katru notifikācijas darbību, pieskaroties tai. Izvēlieties trīs darbības, kuras rādīs kompaktā notifikācijā, izmantojot rūtiņas labajā pusē Piektā darbības poga Ceturtā darbības poga Trešā darbības poga Otrā darbības poga Pirmā darbības poga - Piemērot video attēlu, kuru rāda notifikācijā, no 16:9 uz 1:1 proporciju (iespējams, ka attēls būs izstiepts) + Piemērot video attēlu, kuru rāda notifikācijā, no 16:9 uz 1:1 proporciju (iespējams, attēls būs izstiepts) Piemērot video attēlu 1:1 proporcijai - Rādīt opciju, atskaņot video ar Kodi mediju centru + Rādīt opciju atskaņot video ar Kodi mediju centru Rādīt \"Atskaņot ar Kodi\" opciju Instalēt trūkstošo Kore aplikāciju\? Atskaņot ar Kodi @@ -589,10 +589,10 @@ Abonēts Abonēt Izmantot ārējo audio atskaņotāju - Noņem skaņu dažās rezolūcijās + Noņem skaņu dažās izšķirtspējās Izmantot ārējo video atskaņotāju Kopīgot ar - Rāda rezultātus priekš: %s + Tiek rādīti rezultāti priekš: %s Vai jūs domājāt \"%1$s\"\? Iestatījumi Meklēt @@ -638,7 +638,7 @@ Rādīt krāsainas lentes virs attēliem, norādot to avotu: sarkana - tīkls, zila - disks, zaļa - atmiņa “Krātuves Piekļuves Sistēma” ir neatbalstīta uz Android KitKat un zemākām versijām Ieslēgt teksta atlasīšanu video aprakstā - Nav izvēlēts noklusējuma lejupielādes mape, izvēlaties to tagad + Lejupielādes mape vēl nav iestatīta, izvēlieties noklusējuma lejupielādes mapi Pārvelciet objektus, lai tos noņemtu Rādīt noskatītos video Lokālie meklēšanas ieteikumi @@ -680,4 +680,9 @@ Lejupielādes pabeigtas Tagad varat atlasīt tekstu video aprakstā. + Notifikācijas + Izmainīt ielādēšanas intervāla izmēru (pašlaik %s). Zemāka vērtība var paātrināt sākotnējo video ielādi. Izmainot vērtību, nepieciešams restartēt atskaņotāju. + Avarēt atskaņotāju + Pielāgojiet pašlaik atskaņotās plūsmas notifikāciju + Atskaņotāja notifikācija \ No newline at end of file diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index c38e56671..edbc7785e 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -258,8 +258,8 @@ ബാക്ക്ഗ്രൗണ്ടിലേക്ക്‌ മാറുക [അജ്ഞാതം] പുതിയ ന്യൂപൈപ്പ് പതിപ്പിന് വേണ്ടിയുള്ള അറിയിപ്പ് - അപ്ഡേറ്റ് അറിയിപ്പ് - ന്യൂപൈപ്പ് ബാക്ക്ഗ്രൗണ്ട്, പോപ്പപ്പ് പ്ലയറുകൾക്ക് വേണ്ടിയുള്ള അറിയിപ്പുകൾ + ആപ്പ് അപ്ഡേറ്റ് ചെയ്യാനുള്ള അറിയിപ്പ് + ന്യൂപൈപ്പ് പ്ലേയറിന് വേണ്ടിയുള്ള അറിയിപ്പുകൾ ന്യൂപൈപ്പ് അറിയിപ്പ് ഫയൽ ഒരിക്കൽ മാത്രം @@ -310,7 +310,7 @@ സ്ഥിര കന്റെന്റ് രാജ്യം അനുയോജ്യമല്ലാത്ത URL പോപ്പപ്പ്/ബാക്ക്ഗ്രൗണ്ട് ബട്ടൺ അമർത്തുമ്പോൾ \"വിശദാംശങ്ങൾ\" എന്ന ടിപ് കാണിക്കും - \"ഹോൾഡ് ടു അപ്പെൻഡ്\" എന്ന ടിപ് കാണിക്കുക + \"ക്യൂവിൽ കയറ്റാൻ പിടിക്കുക\" എന്ന ടിപ് കാണിക്കുക \'അടുത്ത\' , \'സമാനമായ\' വീഡിയോകൾ കാണിക്കുക ഓട്ടോപ്ലേ ഡൗൺലോഡ് @@ -588,7 +588,7 @@ പക്വതയുള്ള ഉള്ളടക്കം മറയ്ക്കുന്ന \"നിയന്ത്രിത മോഡ്\" യൂട്യൂബ് നൽകുന്നു കുട്ടികൾക്ക് അനുയോജ്യമല്ലാത്ത ഉള്ളടക്കം കാണിക്കുക കാരണം അതിന് പ്രായപരിധി ഉണ്ട് (18+ പോലെ) URL തിരിച്ചറിയാൻ കഴിഞ്ഞില്ല. മറ്റൊരു അപ്ലിക്കേഷൻ ഉപയോഗിച്ച് തുറക്കണോ\? - യാന്ത്രിക-ക്യൂ + തനിയെ ക്യൂവിൽ കയറ്റുക സ്ട്രീം സ്രഷ്ടാവ്, സ്ട്രീം ഉള്ളടക്കം അല്ലെങ്കിൽ ഒരു തിരയൽ അഭ്യർത്ഥന എന്നിവയെക്കുറിച്ചുള്ള കൂടുതൽ വിവരങ്ങൾ ഉൾക്കൊള്ളുന്ന മെറ്റാ വിവര ബോക്സുകൾ മറയ്ക്കുന്നതിന് ഓഫാക്കുക മെറ്റാഇൻഫോ കാണിക്കുക വീഡിയോ വിവരണവും അധിക വിവരങ്ങളും മറയ്ക്കുന്നതിന് ഓഫാക്കുക @@ -653,4 +653,10 @@ തുടങ്ങുന്ന പ്രേധാന പേജ് മുഴുവന്‍ സ്ക്രീനില്‍ കാണിക്കുക ഐറ്റം കളയണം എന്നുണ്ടെല്‍ സ്വൈപ്പ് ചൈയ്യുക മിനി പ്ലേയര്‍ -ല്‍ വീഡിയോ -ക്കള്‍ ഒരിക്കലും സ്റ്റാര്‍ട്ട് ചൈയ്യരുത് , പക്ഷേ നേരെ ഫുള്‍ സ്ക്രീന്‍ മോഡിലെക് മാറും .ഓട്ടോ റൊട്ടേഷന്‍ ലോക്ക് ചെയിത്തിട്ടുണ്ടെങ്കില്‍ നിലവിലെ ഫുള്‍ സ്ക്രീന്‍ നില്‍ നിന്നും മിനി പ്ലായേറിലെക് മാറാന്‍ ആകും + നിലവിൽ കാണുന്ന സ്ട്രീം അറിയിപ്പ് ക്രമീകരിക്കുക + അറിയിപ്പുകൾ + പ്ലേയർ അറിയിപ്പ് + പുതിയ സ്ട്രീമുകൾ + ലോഡ് ഇടവേള മാറ്റുക (ഇപ്പൊൾ %s). കുറഞ്ഞ മൂല്യം വീഡിയോ വേഗത്തിൽ ലോഡ് ചെയ്യാൻ ഇടയാക്കാം. മാറ്റങ്ങൾ പ്രാഭല്യതിൽ വരുത്താൻ പ്ലേയർ പുനരാരംഭിക്കണം. + പ്ലേയർ തകർക്കുക \ No newline at end of file diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 92b24fbc8..58b4cb08c 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -303,8 +303,8 @@ \n2. Gå til denne nettadressen: %1$s \n3. Logg inn når forespurt \n4. Kopier profil-nettadressen du ble videresendt til.
- Unøyaktig blafring lar spilleren søke posisjoner raskere med redusert presisjon. Å søke i 5, 15 eller 25 sekunder fungerer ikke med dette. - Skru av for å stoppe innlasting av miniatyrbilder, noe som sparer data- og minnebruk. Endring av dette vil tømme både disk- og minne-hurtiglager. + Unøyaktig blafring lar spilleren søke posisjoner raskere med redusert presisjon. Å søke i 5, 15 eller 25 sekunder fungerer ikke med dette + Skru av for å stoppe innlasting av miniatyrbilder, noe som sparer data- og minnebruk. Endring av dette vil tømme både disk- og minne-hurtiglager Fortsett fullendt (ikke-repeterende) avspillingskø ved å legge til en relatert strøm Minnelekkasjeoppsyn kan forårsake programmet å opptre uresponsivt under haugdumping Rapporter feil utenfor livssyklusen @@ -532,7 +532,7 @@ Originaltekster fra tjenester vil vises for elementer i strømmen Sjekk om det allerede eksisterer et problem som diskuterer ditt krasj. Når du oppretter duplikatbilletter, tar du tid fra oss som vi kan bruke på å fikse den faktiske feilen. Du kan maksimalt velge tre handlinger som skal vises i kompaktvarselet! - Rediger hver varslingshandling nedenfor ved å trykke på den. Velg opptil tre av dem som skal vises i det kompakte varselet ved å bruke avmerkingsboksene til høyre. + Rediger hver varslingshandling nedenfor ved å trykke på den. Velg opptil tre av dem som skal vises i det kompakte varselet ved å bruke avmerkingsboksene til høyre Skaler videominiatyrbildet som vises i varselet fra 16:9 til 1:1 sideforhold (kan føre til forvrengninger) Tilgjengelig i noen tjenester, det er vanligvis mye raskere, men kan gi et begrenset antall elementer, og ofte ufullstendig info (f.eks. ingen varighet, elementtype, eller sanntidsstatus). Hent fra dedikert strøm når tilgjengelig @@ -575,7 +575,7 @@ Regner ut sjekksum Merknad for videosjekksummeringsframdrift Videosjekksumsmerknad - Slå av for å skjule metainformasjonsbokser med tilleggsinformasjon om strømskaperen, strøminnhold eller en søkeforespørsel. + Slå av for å skjule metainformasjonsbokser med tilleggsinformasjon om strømskaperen, strøminnhold eller en søkeforespørsel Vis metainfo Nylige Ingen programmer på enheten din kan åpne dette @@ -672,7 +672,6 @@ Viser et krasjalternativ ved bruk av avspilleren Det oppstod en feil. Sjekk merknaden. Festet kommentar - Spilles allerede i bakgrunnen Feilrapport-merknad Merknader for innrapportering av feil NewPipe-feil. Trykk for å rapportere. @@ -682,6 +681,4 @@ Installer en filbehandler som støtter lagringstilgangsrammeverk først. LeakCanary er ikke tilgjengelig ExoPlayer-forvalg - Juster toneart etter musikalske halvtoner - Tempo-steg \ No newline at end of file diff --git a/app/src/main/res/values-nl-rBE/strings.xml b/app/src/main/res/values-nl-rBE/strings.xml index 376b43319..3b54c56f5 100644 --- a/app/src/main/res/values-nl-rBE/strings.xml +++ b/app/src/main/res/values-nl-rBE/strings.xml @@ -622,4 +622,22 @@ Niet tonen Reacties zijn uitgeschakeld Toon afbeeldingsindicatoren + Speler melding + Configureer actieve stream melding + Meldingen + Nieuwe streams + Meldingen over nieuwe streams voor abonnementen + NewPipe meldt een fout, tik om te rapporteren + Verander het laadinterval (momenteel %s). Een lagere waarde kan het laden van video versnellen. Vereist een herstart van de speler. + + %s nieuwe stream + %s nieuwe streams + + Foutrapport melding + Stream details aan het laden… + Een fout is opgetreden, zie melding + Crash de speler + Meldingen om fouten te rapporteren + Verwerken... Dit kan even duren + LeakCanary is niet beschikbaar \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index dd405963a..7bdc902a5 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -677,7 +677,6 @@ NewPipe meldt fout, tik voor bericht Foutmelding Maak een foutmelding - Speelt al op de achtergrond Korte foutmelding weergeven Er is geen geschikte bestandsbeheerder gevonden voor deze actie. \nInstalleer een bestandsbeheerder of probeer \'%s\' uit te schakelen in de download instellingen. @@ -686,7 +685,29 @@ Vastgemaakt commentaar LeakCanary is niet beschikbaar Verander de laad interval tijd (nu %s). Een lagere waarde kan het initiële laden van de video versnellen. De wijziging vereist een herstart van de speler. - Pas de toonhoogte aan met muzikale halve tonen - Tempo stap ExoPlayer standaard + Speler melding + Configureer meldingen van de huidige spelende stream + Meldingen + Nieuwe streams + Meldingen over nieuwe streams van abonnementen + Bezig met laden van stream details… + Controleer op nieuwe streams + Meldingen over nieuwe streams + Melding over nieuwe streams van abonnementen + Frequentie van controleren + Vereiste netwerk connectie + Elk netwerk + Meldingen zijn uitgeschakeld + Ontvang een melding + , + Alles in-/uitschakelen + Percentage + Halve toon + + %s nieuwe stream + %s nieuwe streams + + Je bent nu geabonneerd op dit kanaal + Alle gedownloade bestanden van schijf wissen\? \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index cfa995212..a09549fcf 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -701,14 +701,11 @@ \nZainstaluj menedżer plików lub spróbuj wyłączyć „%s” w ustawieniach pobierania. Nie znaleziono odpowiedniego menedżera plików dla tej akcji. \nZainstaluj menedżer plików zgodny z Storage Access Framework. - Już jest odtwarzane w tle Przypięty komentarz LeakCanary jest niedostępne Rozmiar interwału ładowania odtwarzania Zmień rozmiar interwału ładowania (aktualnie %s). Niższa wartość może przyspieszyć początkowe ładowanie wideo. Zmiany wymagają ponownego uruchomienia odtwarzacza domyślny ExoPlayera - Dostosuj wysokość półtonami - Krok tempa Powiadomienie odtwarzacza Skonfiguruj powiadomienie aktualnie odtwarzanego strumienia Uruchom sprawdzenie nowych strumieni @@ -733,4 +730,13 @@ Otrzymuj powiadomienia , Przełącz wszystkie + Procent + Półton + Wybrany strumień nie jest obsługiwany przez zewnętrzne odtwarzacze + Strumienie, których jeszcze nie da się pobrać, nie są wyświetlane + Brak dostępnych strumieni audio dla zewnętrznych odtwarzaczy + Brak dostępnych strumieni wideo dla zewnętrznych odtwarzaczy + Wybierz jakość dla zewnętrznych odtwarzaczy + Nieznany format + Nieznana jakość \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 0850034f6..4a743cd55 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -682,12 +682,9 @@ Mostrar um snackbar de erro Nenhum gerenciador de arquivos apropriado foi encontrado para esta ação. \nInstale um gerenciador de arquivos ou tente desativar \'%s\' nas configurações de download. - Já está tocando em segundo plano Comentário fixado O LeakCanary não está disponível - Passo do tempo Altere o tamanho do intervalo de carregamento (atualmente %s). Um valor menor pode acelerar o carregamento inicial do vídeo. As alterações exigem que o player reinicie. - Ajustar o tom por semitons musicais ExoPlayer padrão Notificação do reprodutor Configurar a notificação do fluxo da reprodução atual @@ -711,4 +708,83 @@ Conexão de rede necessária As notificações estão desativadas Seja notificado + SponsorBlock (Em beta, serviço de terceiro) + Categorias do SponsorBlock + Personalize quais segmentos de vídeo pular, junto com suas marcações de cores na barra. + Redefinir cores + Ativar + Desativar + Cor da barra + Patrocinador + Promoção paga, referências pagas e anúncios diretos. Não é usado em auto promoção ou mensagens grátis para causas/criadores/sites/produtos que eles gostam. + Intervalo/Animação de introdução + Um intervalo sem conteúdo real. Pode ser uma pausa, um quadro estático, uma animação repetitiva. Isso não é usado em transições que contém informação. + Finalização/Créditos + Créditos ou quando os cards finais do YouTube aparecem. Não é usado em conclusões informativas. + Lembrete de interação (inscrever-se) + Quando houver um pequeno lembrete para curtir, inscrever-se ou segui-los no meio do conteúdo. Se for longo ou sobre algo específico, deve estar em autopromoção. + Não-pago/Auto promoção + Similar a "patrocinador", mas para auto promoções e segmentos não-pagos. Isso inclui seções sobre vendas, doações ou informações sobre com quem colaboraram. + Música: Seção sem música + Somente para uso em vídeos de música. Isso inclui introduções ou encerramentos em videoclipes. + Enrolação/Piadas + Cenas tangenciais inseridas apenas por enrolação ou humor que não são necessárias para compreender o tópico principal do vídeo. + Pré-visualização/Recapitulação + Recapitulação rápida de episódios anteriores, ou uma prévia do que está chegando mais tarde no vídeo atual. Destinado a clipes editados juntos, não é para resumos falados. + SponsorBlock + Visitar site + Visite o site oficial do SponsorBlock. + Pular patrocinadores + Use a API SponsorBlock para pular automaticamente os patrocinadores nos vídeos. Atualmente, isso só funciona para vídeos do YouTube. + URL da API + A URL a ser usado ao consultar a API do SponsorBlock. Isso deve ser definido para que SponsorBlock funcione. + Notificar quando os patrocinadores forem ignorados + Mostre uma notificação de brinde quando um patrocinador for ignorado automaticamente. + Ver política de privacidade + Veja a política de privacidade do SponsorBlock. + Este é o URL que será consultado quando o aplicativo precisar saber quais partes de um vídeo devem ser ignoradas.\n\nVocê pode definir o URL oficial clicando na opção \'Usar oficial\' abaixo, embora seja altamente recomendável que você veja a política de privacidade do SponsorBlock antes de usá-lo. + Política de privacidade do SponsorBlock + https://sponsor.ajay.app/ + https://sponsor.ajay.app/api/ + https://gist.github.com/ajayyy/aa9f8ded2b573d4f73a3ffa0ef74f796 + Patrocinador ignorado + Intervalo/Introdução ignorado + Finalização/Créditos ignorado + Lembrete de inscrição ignorado + Não-pago/Auto promoção ignorado + Sem música ignorado + Enrolação/Piadas ignorado + Pré-visualização/Recapitulação ignorado + Alternar patrocinadores + Limpar lista de canais permitidos + Limpe a lista de canais que o SponsorBlock irá ignorar. + SponsorBlock habilitando + SponsorBlock desabilitado + A lista de canais permitidos foi limpa. + Canal adicionado à lista de canais permitidos + Canal removido da lista de canais permitidos + Tem certeza de que deseja excluir a lista de canais permitidos? + Tem certeza de que deseja redefinir as cores das categorias? + Cores redefinidas. + Extras + Ajustes, correções e outros ajustes diversos pertencem aqui. + Configurações experimentais + Ativar player local (alfa) + Use um player embutido para reprodução local. Isso ainda está em desenvolvimento inicial, então provavelmente haverá muitos problemas, incluindo conflitos com o player existente + Forçar tela cheia automaticamente + Se ativado, quando o dispositivo estiver no modo paisagem, força o modo de tela cheia mesmo se o dispositivo for um tablet ou TV. + Desativar relatório de erros + Impede que todas as telas de relatório de erros apareçam. Isso pode fazer com que o aplicativo se comporte inesperadamente. USE POR SUA CONTA E RISCO! + Mostrar contagem de não gostei + Utiliza a API ReturnYouTubeDislike para exibir o número de não gostei de um vídeo. Isso só funciona com vídeos do YouTube.\nAVISO: seu endereço IP ficará visível para a API. Use por sua conta e risco. + Por cento + Semitom + A transmissão selecionada não é compatível com players externos + Nenhum áudio de transmissão está disponível para players externos + As transmissão que ainda não são suportados pelo downloader não são exibidos + Nenhum vídeo de transmissão está disponível para players externos + Selecione a qualidade para players externos + Formato desconhecido + Qualidade desconhecida + Tamanho do intervalo de carregamento da reprodução \ No newline at end of file diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 43e782251..9a9adad52 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -36,7 +36,7 @@ Legendas Repor predefinições Ocorreu um erro: %1$s - Popup + Pop-up Descarga NewPipe Devido às restrições de ExoPlayer, a duração da pesquisa foi definida para %d segundos Sobrescrever @@ -683,11 +683,8 @@ Nenhum gestor de ficheiros apropriado foi encontrado para esta ação. \nPor favor, instale um gestor de ficheiros compatível com o Storage Access Framework. Comentário fixado - Já está a reproduzir em segundo plano LeakCanary não está disponível Altere o tamanho do intervalo de carregamento (atualmente %s). Um valor menor pode acelerar o carregamento inicial do vídeo. Se fizer alterações é necessário reiniciar. - Ajustar o tom por semitons musicais - Passo do tempo Predefinido do ExoPlayer Notificações A carregar detalhes do fluxo… @@ -711,4 +708,13 @@ Agora assinou este canal , Novos fluxos + Por cento + Semitom + Não estão disponíveis transmissões de vídeo a reprodutores externos + As transmissões que ainda não são suportadas para descarregamento não são mostradas + A transmissão selecionada não é suportada por reprodutores externos + Não estão disponíveis transmissões de áudio a reprodutores externos + Formato desconhecido + Qualidade desconhecida + Selecione a qualidade para reprodutores externos \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 9e1a80916..5453b1fa4 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -554,7 +554,6 @@ URL não reconhecido. Abrir com outra aplicação\? Enfileiramento automático Embaralhar - Notificação Apenas em Wi-Fi Nada Mudar de um reprodutor para outro pode substituir a sua fila @@ -683,13 +682,10 @@ \nPor favor, instale um gestor de ficheiros ou tente desativar \'%s\' nas configurações de descarregar. Nenhum gestor de ficheiros apropriado foi encontrado para esta ação. \nPor favor, instale um gestor de ficheiros compatível com o Storage Access Framework. - Já está a reproduzir em segundo plano Comentário fixado LeakCanary não está disponível - Ajustar o tom por semitons musicais Predefinido do ExoPlayer Altere o tamanho do intervalo de carregamento (atualmente %s). Um valor menor pode acelerar o carregamento inicial do vídeo. Se fizer alterações é necessário reiniciar. - Passo do tempo Notificação do reprodutor Configurar a notificação da reprodução do fluxo atual Notificações @@ -712,4 +708,13 @@ Seja notificado Notificações são desativadas , + Por cento + Semitom + As transmissões que ainda não são suportadas para descarregamento não são mostradas + Não estão disponíveis transmissões de áudio a reprodutores externos + Não estão disponíveis transmissões de vídeo a reprodutores externos + Selecione a qualidade para reprodutores externos + Formato desconhecido + Qualidade desconhecida + A transmissão selecionada não é suportada por reprodutores externos \ No newline at end of file diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index e83fc5462..fe2292ef7 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -111,7 +111,7 @@ Nu s-a putut modifica abonamentul Nu s-a putut actualiza abonamentul Abonamente - Ce este nou + Noutăți Istoric de căutări Stochează local căutările Istoricul vizionărilor @@ -123,7 +123,7 @@ Istoric și cache Anulare Notificare NewPipe - Notificări pentru playerele de fundal și popup NewPipe + Notificări pentru playerul NewPipe Fără rezultate Nimic aici în afară de sunetul greierilor Fără abonați @@ -144,12 +144,12 @@ %s videoclipuri %s de videoclipuri - Descărcați + Descărcări Caractere permise în numele fișierelor Caracterele nevalabile sunt înlocuite cu această valoare Caracter de înlocuire Litere și cifre - Caracterele cele mai speciale + Caractere speciale Despre NewPipe Licențe terță-parte © %1$s de %2$s sub %3$s @@ -175,7 +175,7 @@ Top 50 Noi și populare Niciun player pentru streaming găsit. (Totuși, puteți instala VLC). - Descărcați fișierul de vizionat + Descărcați fișierul de flux Arată informații Playlist-uri salvate Salvează în @@ -306,7 +306,7 @@ Mărire Subtitrări Modificați scara textului de legendă a playerului și stilurile de fundal. Necesită repornirea aplicației pentru a intra în vigoare - NewPipe este un software liber cu copyleft: îl puteți folosi, studia, partaja și îmbunătăți în voie. Mai exact, îl puteți redistribui și/sau modifica în conformitate cu termenii Licenței Publice Generale GNU, așa cum a fost publicată de Free Software Foundation, fie versiunea 3 a Licenței, fie (la alegerea dumneavoastră) orice versiune ulterioară. + NewPipe este un software liber cu copyleft: îl puteți folosi, studia, distribui și îmbunătăți în voie. Mai exact, îl puteți redistribui și/sau modifica în conformitate cu termenii Licenței Publice Generale GNU, așa cum a fost publicată de Free Software Foundation, fie versiunea 3 a Licenței, fie (la alegerea dumneavoastră) orice versiune ulterioară. Politica de confidențialitate a NewPipe Proiectul NewPipe ia foarte în serios confidențialitatea dumneavoastră. Prin urmare, aplicația nu colectează niciun fel de date fără consimțământul dumneavoastră. \nPolitica de confidențialitate a NewPipe explică în detaliu ce date sunt trimise și stocate atunci când trimiteți un raport de eroare. @@ -353,7 +353,7 @@ Cookie-urile reCAPTCHA au fost șterse Ștergeți cookie-urile reCAPTCHA Notificări pentru progresul hashing-ului video - Notificare video Hash + Notificare hash video Artiști Albume Melodii @@ -505,7 +505,7 @@ Generați un nume unic Descărcare eșuată Acțiune refuzată de sistem - Coadă + Puneți în coadă Se recuperează post-procesare În așteptare @@ -553,8 +553,8 @@ \n3. Faceți clic pe \"Toate datele incluse\", apoi pe \"Deselectați totul\", apoi selectați doar \"Abonamente\" și faceți clic pe \"OK\" \n4. Faceți clic pe \"Pasul următor\" și apoi pe \"Creați exportul\" \n5. Faceți clic pe butonul \"Descărcare\" după ce acesta apare -\n6. Faceți clic pe IMPORT FIȘIER de mai jos și selectați fișierul zip descărcat -\n7. [În cazul în care importul zip eșuează] Extrageți fișierul .csv (de obicei sub \"YouTube and YouTube Music/subscriptions/subscriptions.csv\"), faceți clic pe IMPORT FIȘIER de mai jos și selectați fișierul csv extras +\n6. Faceți clic pe IMPORT FIȘIER de mai jos și selectați fișierul .zip descărcat +\n7. [În cazul în care importul .zip eșuează] Extrageți fișierul .csv (de obicei sub \"YouTube and YouTube Music/subscriptions/subscriptions.csv\"), faceți clic pe IMPORT FIȘIER de mai jos și selectați fișierul csv extras Textele originale din servicii vor fi vizibile în elementele de flux Raportați erori în afara ciclului de viață Afișați scurgeri de memorie @@ -679,16 +679,48 @@ Procesarea.. Poate dura un moment Verifică dacă există actualizări Verifică manual dacă există versiuni noi - Se redă deja pe fundal Comentariu lipit Notificare cu raport de eroare Afișează opțiunea de a întrerupe atunci când utilizați playerul - A apărut o eroare, verifică notificarea + A apărut o eroare, consultați notificarea NewPipe a întămpinat o eroare, apăsați ca să raportați Se verifică actualizări… Notificări pentru a raporta erori - Crează o notificare de eroare - Arată \"închiderea bruscă a player-ului video\" - Arată o eroare de tip snackbar - Elemente de flux noi + Creați o notificare de eroare + Afișează \"Dați crash playerului\" + Afișați o eroare de tip snackbar + Elemente noi în flux + Notificarea player-ului + Configurați notificarea fluxului de redare curent + Fluxuri noi + + %s flux nou + %s fluxuri noi + %s de fluxuri noi + + Se încarcă detaliile fluxului… + Notificări de fluxuri noi + Notificare despre fluxuri noi din abonamente + Frecvența verificării + Conexiune de rețea necesară + Orice rețea + Ștergeți toate fișierele descărcate de pe disc\? + Notificările sunt dezactivate + Procent + Semiton + Implicit ExoPlayer + Modificați dimensiunea intervalului de încărcare (în prezent %s). O valoare mai mică poate accelera încărcarea inițială a videoclipului. Modificările necesită o repornire a playerului. + Dați crash player-ului + LeakCanary nu este disponibil + Notificări + Notificări despre noi fluxuri pentru abonamente + Executați verificarea pentru fluxuri noi + Nu a fost găsit niciun manager de fișiere adecvat pentru această acțiune. +\nVă rugăm să instalați un manager de fișiere sau să încercați să dezactivați \'%s\' în setările de descărcare. + Nu a fost găsit niciun manager de fișiere adecvat pentru această acțiune. +\nVă rugăm să instalați un manager de fișiere compatibil cu Storage Access Framework. + Primiți notificări + V-ați abonat la acest canal + Comutați toate + , \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index fc4c03f42..64351c7d3 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -691,8 +691,6 @@ Проверить обновления Проверка обновлений… Новое на канале - Отчёт об ошибках плеера - Подробные отчёты об ошибках плеера вместо коротких всплывающих сообщений (полезно при диагностике проблем) Уведомления Новые видео Уведомления о новых видео в подписках @@ -718,11 +716,8 @@ \nПожалуйста, установите файловый менеджер, или попробуйте отключить \'%s\' в настройках загрузок. Для этого действия не найдено подходящего файлового менеджера. \nПожалуйста, установите файловый менеджер, совместимый со Storage Access Framework (SAF). - Уже проигрывается в фоне Закреплённый комментарий LeakCanary недоступна - Регулировка высоты тона по музыкальным полутонам - Шаг темпа Стандартное значение ExoPlayer Изменить размер интервала загрузки (сейчас %s). Меньшее значение может ускорить начальную загрузку видео. Изменение значения потребует перезапуска плеера. Загрузка деталей трансляции… @@ -730,4 +725,13 @@ Удалить все загруженные файлы\? Уведомления плеера , + Полутон + Проценты + Выбранная трансляция не поддерживается внешними проигрывателями + Нет ни одного доступного видео потока для внешних проигрывателей + Были скрыты трансляции, которые пока ещё не поддерживаются загрузчиком + Неизвестный формат + Нет ни одного доступного аудио потока для внешних проигрывателей + Выберите качество для внешних проигрывателей + Неизвестное качество \ No newline at end of file diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml index 38a7c3303..2fc18cb6f 100644 --- a/app/src/main/res/values-sc/strings.xml +++ b/app/src/main/res/values-sc/strings.xml @@ -683,10 +683,7 @@ \nPro praghere installa unu gestore de documentos cumpatìbile cun su \"Sistema de Atzessu a s\'Archiviatzione\". Faghe serrare su riproduidore Cummentu apicadu - Giai in riprodutzione in s\'isfundu LeakCanary no est a disponimentu - Règula s\'intonatzione in base a sos semitonos musicales - Passu de tempus Valore ExoPlayer predefinidu Muda sa mannària de s\'intervallu de carrigamentu (in custu momentu %s). Unu valore prus bassu diat pòdere allestrare su carrigamentu de incumintzu de su vìdeu. Sas modìficas tenent bisòngiu de torrare a allùghere su riproduidore. Cunfigura sa notìfica de su flussu in cursu de riprodutzione @@ -711,4 +708,13 @@ Notìficas Flussos noos Iscantzellare totu sos archìvios iscarrigados dae su discu\? + Pertzentuale + Semitonu + Seletziona sa calidade pro sos letores esternos + Sos flussos chi non sunt galu suportados dae s\'iscarrigadore non sunt ammustrados + Non b\'est perunu flussu sonoru a disponimentu pro letores esternos + Non bi sunt flussos de vìdeu a disponimentu pro letores esternos + Formadu disconnotu + Calidade disconnota + Su flussu seletzionadu no est galu suportadu dae letores esternos \ No newline at end of file diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 9b7a62145..bfba46a5c 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -79,7 +79,7 @@ Prevzaté Hlásenie o chybe Aplikácia/UP zlyhalo - Čo:\\nPožiadavka:\\nJazyk obsahu:\\nKrajina Obsahu:\\nJazyk Aplikácie:\\nSlužba:\\nČas v GMT:\\nBalík:\\nVerzia:\\nVerzia OS: + Čo:\\nPožiadavka:\\nJazyk obsahu:\\nKrajina Obsahu:\\nJazyk Aplikácie:\\nSlužba:\\nČas v GMT:\\nBalík:\\nVerzia:\\nOS: Výzva reCAPTCHA Čierna Všetko @@ -647,8 +647,6 @@ Pri každom sťahovaní sa zobrazí výzva kam uložiť súbor Nie je nastavený adresár na sťahovanie, nastavte ho teraz Označiť ako videné - Načítavanie podrobností o kanáli… - Chyba pri zobrazení podrobností kanála Vypnuté Zapnuté Režim tabletu @@ -698,8 +696,6 @@ Zobrazí možnosť zlyhania pri používaní prehrávača Zobraziť krátke oznámenie chyby Oznámte chybu - Upraviť výšku poltónov - Krok tempa ExoPlayer preddefinovaný Zmeniť interval načítania (aktuálne %s). Menšia hodnota môže zvýšiť rýchlosť prvotného načítania videa. Zmena vyžaduje reštart. Upozornenia @@ -712,4 +708,19 @@ Začali ste odoberať tento kanál , Zapnúť všetko + Nové streamy + Upozornenia na nové streamy v odberoch + + %s nový stream + %s nové streamy + %s nových streamov + + Skontrolovať nové streamy + Upozornenia na nové streamy + Upozorniť na nové streamy z odberov + Akákoľvek sieť + Dostávať upozornenia + Poltón + Nahrávanie podrobností streamu… + Percent \ No newline at end of file diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 3b170795b..68981868c 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -4,9 +4,9 @@ Нема плејера токова. Инсталирати ВЛЦ\? Инсталирај Откажи - Отвори у прегледачу + Отвори у претраживачу Подели - Преузимање + Преузми Тражи Подешавања Да ли сте мислили: „%1$s“\? @@ -61,7 +61,7 @@ Аудио Покушај поново Уживо - Тапните на лупу да започнете. + Кликните на лупу да започнете. Почни Паузирај Обриши @@ -515,7 +515,7 @@ Редослед активног плејера биће замењен Пребацивање на други плејер може променити ваш редослед Питај за потврду пре пражњења редоследа - Време за премотавања напред/назад + Период премотавања напред/назад ноћна тема Андроид ће прилагодити боју обавештења према главној боји на сличици (није доступно на свим уређајима) Обоји обавештења diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 63af6d4d6..53cd6909b 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -682,19 +682,16 @@ \nInstallera en filhanterare eller testa att inaktivera \'%s\' i nedladdningsinställningarna. Ingen lämplig filhanterare hittades för denna åtgärd. \nInstallera en filhanterare som är kompatibel med Storage Access Framework. - Spelas redan i bakgrunden Fäst kommentar LeakCanary är inte tillgänglig - Justera tonhöjden med musikaliska halvtoner ExoPlayer standard Ändra inläsningsintervallets storlek (för närvarande %s). Ett lägre värde kan påskynda den första videoinläsningen. Ändringar kräver omstart av spelaren. - Temposteg Validera frekvens Kräver nätverksanslutning Alla nätverk Radera alla nedladdade filer från disken\? Notifikationer är avstängda - Bli medelad + Bli meddelad Du har nu prenumenerat till denna kanalen Notifikationer om nya strömmar för prenumenanter @@ -703,7 +700,7 @@ Konfigurera meddelande om aktuell ström som spelas upp Kör leta efter nya strömmar - Medela om nya strömmar från prenumenanter + Meddela om nya strömmar från prenumeranter Notifikationer Nya strömmar Laddar strömdetaljer… @@ -711,4 +708,10 @@ , Spelaravisering Växla alla + Procent + Halvton + Inga videoströmmar tillgängliga för externa spelare + Okänd kvalitet + Inga ljudströmmar tillgängliga för externa spelare + Okänt format \ No newline at end of file diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index d7c920f2b..827da452a 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -253,7 +253,6 @@ இயக்குதலைத் மறுதொடர் நி நிகழ்வு ஏற்கனவே உள்ளது - அறிவிப்பு யூடியூபின் \"கட்டுப்பாடு பயன்முறை\"ஐ இயக்கு பாடல்கள் பிழைகளைப் புகாரளிக்க அறிவிப்புகள் diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index 60542e0b6..3958e02b0 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -371,7 +371,6 @@ reCAPTCHA సవాలు reCAPTCHA సవాలు అభ్యర్థించబడింది ప్లేజాబితాను ఎంచుకోండి - ఇప్పటికే వెనుకగా ప్లే అవుతోంది డాటాబేసుని ఎగుమతిచేయుము యాప్ పునఃప్రారంభించబడిన తర్వాత భాష మారుతుంది ఛానెల్ వివరాలను చూపు diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 958dc5eeb..512790f7c 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1,7 +1,7 @@ - Başlamak için büyüteç simgesine dokunun. - Yayınlanma: %1$s + Başlamak için büyütece dokunun. + %1$s tarihinde yayınlandı Akış oynatıcısı bulunamadı. VLC yüklensin mi\? Yükle İptal @@ -20,7 +20,7 @@ Ses indirme klasörü İndirilen ses dosyaları burada depolanır Ses dosyaları için indirme klasörünü seç - Öntanımlı çözünürlük + Varsayılan çözünürlük Kodi ile oynat Eksik Kore uygulaması yüklensin mi\? \"Kodi ile oynat\" seçeneğini göster @@ -103,7 +103,7 @@ Açılan pencerenin son boyutunu ve konumunu hatırla Bazı çözünürlüklerde sesi kaldırır Arama önerileri - Ararken gösterilecek önerileri seç + Arama yaparken gösterilecek önerileri seçin En iyi çözünürlük NewPipe Hakkında Üçüncü Taraf Lisansları @@ -282,7 +282,7 @@ \nSürdürmek istiyor musunuz\? Küçük resimleri yükle Küçük resimlerin yüklenmesini önleyerek veri ve hafıza kullanımından tasarruf etmek için kapatın. Değişiklikler, hem bellek içi hem de diskteki resim önbelleğini temizler - Resim önbelleği temizlendi + Resim önbelleği silindi Önbelleğe alınmış üstverileri temizle Önbelleğe alınmış tüm web sayfası verilerini kaldır Üstveri önbelleği temizlendi @@ -541,7 +541,7 @@ Lütfen hatanızı tartışan sorunun var olup olmadığını kontrol edin. Yinelenen istekler oluştururken, bizden asıl hatayı düzeltmek için harcayabileceğimiz zamanı alırsınız. Henüz oynatma listesi yer imleri yok Asla - Yalnızca Wi-Fi\'de + Yalnızca Wi-Fi Oynatmayı kendiliğinden başlat — %s Oynatma kuyruğu URL tanınamadı. Başka bir uygulamayla açılsın mı\? @@ -682,13 +682,10 @@ Hata raporları için bildirimler Oynatıcı kullanırken çöktürme seçeneği gösterir Oynatıcıyı çöktür - Zaten arka planda oynuyor Sabitlenmiş yorum LeakCanary yok Yükleme ara boyutunu değiştir (şu anda %s). Düşük bir değer videonun ilk yüklenişini hızlandırabilir. Değişiklikler oynatıcının yeniden başlatılmasını gerektirir. ExoPlayer öntanımlısı - Tempo adımı - Perdeyi müzikal yarım tonlarla uyarla Yeni akış bildirimleri Bildirimler @@ -711,4 +708,14 @@ İndirilen tüm dosyalar diskten silinsin mi\? , Tümünü değiştir + Yüzde + Ara ton + Seçilen yayın harici oynatıcılar tarafından desteklenmiyor + İndirici tarafından henüz desteklenmeyen yayınlar gösterilmez + Harici oynatıcılar için ses yayını yok + Harici oynatıcılar için video yayını yok + Harici oynatıcılar için kalite seçin + Bilinmeyen biçim + Bilinmeyen kalite + Oynatma yükleme aralığı boyutu \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f2a3e986e..50e1b00b7 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -58,7 +58,6 @@ Завантаження Звіт про помилку Усе - Так Вимкнено Збій застосунку/інтерфейсу Ваш коментар (англійською): @@ -93,7 +92,7 @@ Не вдалося оновити підписку Підписки Новинки - У фоновому режимі + Фоново У вікні Типова роздільність вікна Не всі пристрої можуть відтворювати 2K/4K-відео @@ -699,11 +698,8 @@ \nУстановіть файловий менеджер, сумісний зі Storage Access Framework. Показати панель помилок Створити сповіщення про помилку - Уже відтворюється у фоновому режимі Закріплений коментар LeakCanary недоступний - Крок темпу - Регулювання висоти звуку за музичними півтонами Типовий ExoPlayer Змінити розмір інтервалу завантаження (наразі %s). Менше значення може прискорити початкове завантаження відео. Зміни вимагають перезапуску програвача. Ви підписалися на цей канал @@ -729,4 +725,14 @@ Сповіщення про нові трансляції Частота перевірки Необхідний тип з\'єднання + Відсоток + Напівтон + Немає аудіотрансляцій доступних для зовнішніх програвачів + Немає доступних відеотрансляцій для зовнішніх програвачів + Невідомий формат + Трансляції, які ще не підтримуються завантажувачем, не показані + Вибрана трансляція не підтримується зовнішніми програвачами + Виберіть якість для зовнішніх програвачів + Невідома якість + Розмір інтервалу завантаження відтворення \ No newline at end of file diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml index bdad22174..8fa00d0d8 100644 --- a/app/src/main/res/values-v21/styles.xml +++ b/app/src/main/res/values-v21/styles.xml @@ -12,21 +12,31 @@ + + + + - - + +