1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-18 21:35:51 -04:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Qstick 72062704ff Aphrodite merged health check 2020-09-26 22:49:59 -04:00
1345 changed files with 32441 additions and 53428 deletions
+9 -12
View File
@@ -110,13 +110,13 @@ dotnet_diagnostic.SA1643.severity = none
dotnet_diagnostic.SA1648.severity = none dotnet_diagnostic.SA1648.severity = none
dotnet_diagnostic.SA1649.severity = none dotnet_diagnostic.SA1649.severity = none
dotnet_diagnostic.SA1651.severity = none dotnet_diagnostic.SA1651.severity = none
dotnet_diagnostic.SX1101.severity = warning
dotnet_diagnostic.SX1309.severity = warning dotnet_diagnostic.SX1309.severity = warning
# Microsoft Analyzers that fail and need to be sorted thru # Microsoft Analyzers that fail and need to be sorted thru
dotnet_diagnostic.ASP0000.severity = suggestion dotnet_diagnostic.ASP0000.severity = suggestion
dotnet_diagnostic.CA1000.severity = suggestion dotnet_diagnostic.CA1000.severity = suggestion
dotnet_diagnostic.CA1001.severity = suggestion dotnet_diagnostic.CA1001.severity = suggestion
dotnet_diagnostic.CA1002.severity = suggestion
dotnet_diagnostic.CA1003.severity = suggestion dotnet_diagnostic.CA1003.severity = suggestion
dotnet_diagnostic.CA1008.severity = suggestion dotnet_diagnostic.CA1008.severity = suggestion
dotnet_diagnostic.CA1010.severity = suggestion dotnet_diagnostic.CA1010.severity = suggestion
@@ -163,16 +163,10 @@ dotnet_diagnostic.CA1304.severity = suggestion
dotnet_diagnostic.CA1305.severity = suggestion dotnet_diagnostic.CA1305.severity = suggestion
dotnet_diagnostic.CA1307.severity = suggestion dotnet_diagnostic.CA1307.severity = suggestion
dotnet_diagnostic.CA1308.severity = suggestion dotnet_diagnostic.CA1308.severity = suggestion
dotnet_diagnostic.CA1309.severity = suggestion
dotnet_diagnostic.CA1310.severity = suggestion
dotnet_diagnostic.CA1401.severity = suggestion dotnet_diagnostic.CA1401.severity = suggestion
dotnet_diagnostic.CA1416.severity = suggestion
dotnet_diagnostic.CA1507.severity = suggestion dotnet_diagnostic.CA1507.severity = suggestion
dotnet_diagnostic.CA1508.severity = suggestion
dotnet_diagnostic.CA1707.severity = suggestion dotnet_diagnostic.CA1707.severity = suggestion
dotnet_diagnostic.CA1708.severity = suggestion
dotnet_diagnostic.CA1710.severity = suggestion dotnet_diagnostic.CA1710.severity = suggestion
dotnet_diagnostic.CA1711.severity = suggestion
dotnet_diagnostic.CA1712.severity = suggestion dotnet_diagnostic.CA1712.severity = suggestion
dotnet_diagnostic.CA1714.severity = suggestion dotnet_diagnostic.CA1714.severity = suggestion
dotnet_diagnostic.CA1715.severity = suggestion dotnet_diagnostic.CA1715.severity = suggestion
@@ -181,21 +175,27 @@ dotnet_diagnostic.CA1717.severity = suggestion
dotnet_diagnostic.CA1720.severity = suggestion dotnet_diagnostic.CA1720.severity = suggestion
dotnet_diagnostic.CA1721.severity = suggestion dotnet_diagnostic.CA1721.severity = suggestion
dotnet_diagnostic.CA1724.severity = suggestion dotnet_diagnostic.CA1724.severity = suggestion
dotnet_diagnostic.CA1725.severity = suggestion
dotnet_diagnostic.CA1801.severity = suggestion dotnet_diagnostic.CA1801.severity = suggestion
dotnet_diagnostic.CA1802.severity = suggestion dotnet_diagnostic.CA1802.severity = suggestion
dotnet_diagnostic.CA1805.severity = suggestion dotnet_diagnostic.CA1805.severity = suggestion
dotnet_diagnostic.CA1806.severity = suggestion dotnet_diagnostic.CA1806.severity = suggestion
dotnet_diagnostic.CA1810.severity = suggestion dotnet_diagnostic.CA1810.severity = suggestion
dotnet_diagnostic.CA1812.severity = suggestion dotnet_diagnostic.CA1812.severity = suggestion
dotnet_diagnostic.CA1813.severity = suggestion
dotnet_diagnostic.CA1814.severity = suggestion dotnet_diagnostic.CA1814.severity = suggestion
dotnet_diagnostic.CA1815.severity = suggestion dotnet_diagnostic.CA1815.severity = suggestion
dotnet_diagnostic.CA1816.severity = suggestion dotnet_diagnostic.CA1816.severity = suggestion
dotnet_diagnostic.CA1819.severity = suggestion dotnet_diagnostic.CA1819.severity = suggestion
dotnet_diagnostic.CA1820.severity = suggestion
dotnet_diagnostic.CA1821.severity = suggestion
dotnet_diagnostic.CA1822.severity = suggestion dotnet_diagnostic.CA1822.severity = suggestion
dotnet_diagnostic.CA1823.severity = suggestion dotnet_diagnostic.CA1823.severity = suggestion
dotnet_diagnostic.CA1824.severity = suggestion dotnet_diagnostic.CA1824.severity = suggestion
dotnet_diagnostic.CA1825.severity = suggestion
dotnet_diagnostic.CA1826.severity = suggestion
dotnet_diagnostic.CA1827.severity = suggestion
dotnet_diagnostic.CA1828.severity = suggestion
dotnet_diagnostic.CA1829.severity = suggestion
dotnet_diagnostic.CA1834.severity = suggestion
dotnet_diagnostic.CA2000.severity = suggestion dotnet_diagnostic.CA2000.severity = suggestion
dotnet_diagnostic.CA2002.severity = suggestion dotnet_diagnostic.CA2002.severity = suggestion
dotnet_diagnostic.CA2007.severity = suggestion dotnet_diagnostic.CA2007.severity = suggestion
@@ -210,7 +210,6 @@ dotnet_diagnostic.CA2101.severity = suggestion
dotnet_diagnostic.CA2119.severity = suggestion dotnet_diagnostic.CA2119.severity = suggestion
dotnet_diagnostic.CA2153.severity = suggestion dotnet_diagnostic.CA2153.severity = suggestion
dotnet_diagnostic.CA2200.severity = suggestion dotnet_diagnostic.CA2200.severity = suggestion
dotnet_diagnostic.CA2201.severity = suggestion
dotnet_diagnostic.CA2207.severity = suggestion dotnet_diagnostic.CA2207.severity = suggestion
dotnet_diagnostic.CA2208.severity = suggestion dotnet_diagnostic.CA2208.severity = suggestion
dotnet_diagnostic.CA2211.severity = suggestion dotnet_diagnostic.CA2211.severity = suggestion
@@ -256,8 +255,6 @@ dotnet_diagnostic.CA5374.severity = suggestion
dotnet_diagnostic.CA5379.severity = suggestion dotnet_diagnostic.CA5379.severity = suggestion
dotnet_diagnostic.CA5384.severity = suggestion dotnet_diagnostic.CA5384.severity = suggestion
dotnet_diagnostic.CA5385.severity = suggestion dotnet_diagnostic.CA5385.severity = suggestion
dotnet_diagnostic.CA5392.severity = suggestion
dotnet_diagnostic.CA5394.severity = suggestion
dotnet_diagnostic.CA5397.severity = suggestion dotnet_diagnostic.CA5397.severity = suggestion
+1 -2
View File
@@ -1,7 +1,6 @@
{ {
"paths": [ "paths": [
"frontend/src/**/*.js", "frontend/src/**/*.js"
"src/NzbDrone.Core/Localization/Core/*.json"
], ],
"ignored": [ "ignored": [
"**/node_modules/**/*" "**/node_modules/**/*"
+305
View File
@@ -0,0 +1,305 @@
# -*- coding: utf-8; mode: python -*-
##
## Format
##
## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...]
##
## Description
##
## ACTION is one of 'chg', 'fix', 'new'
##
## Is WHAT the change is about.
##
## 'chg' is for refactor, small improvement, cosmetic changes...
## 'fix' is for bug fixes
## 'new' is for new features, big improvement
##
## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc'
##
## Is WHO is concerned by the change.
##
## 'dev' is for developpers (API changes, refactors...)
## 'usr' is for final users (UI changes)
## 'pkg' is for packagers (packaging changes)
## 'test' is for testers (test only related changes)
## 'doc' is for doc guys (doc only changes)
##
## COMMIT_MSG is ... well ... the commit message itself.
##
## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic'
##
## They are preceded with a '!' or a '@' (prefer the former, as the
## latter is wrongly interpreted in github.) Commonly used tags are:
##
## 'refactor' is obviously for refactoring code only
## 'minor' is for a very meaningless change (a typo, adding a comment)
## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...)
## 'wip' is for partial functionality but complete subfunctionality.
##
## Example:
##
## new: usr: support of bazaar implemented
## chg: re-indentend some lines !cosmetic
## new: dev: updated code to be compatible with last version of killer lib.
## fix: pkg: updated year of licence coverage.
## new: test: added a bunch of test around user usability of feature X.
## fix: typo in spelling my name in comment. !minor
##
## Please note that multi-line commit message are supported, and only the
## first line will be considered as the "summary" of the commit message. So
## tags, and other rules only applies to the summary. The body of the commit
## message will be displayed in the changelog without reformatting.
##
## ``ignore_regexps`` is a line of regexps
##
## Any commit having its full commit message matching any regexp listed here
## will be ignored and won't be reported in the changelog.
##
ignore_regexps = [
r'@minor', r'!minor',
r'@cosmetic', r'!cosmetic',
r'@refactor', r'!refactor',
r'@wip', r'!wip',
r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:',
r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:',
r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$',
r'^$', ## ignore commits with empty messages
]
## ``section_regexps`` is a list of 2-tuples associating a string label and a
## list of regexp
##
## Commit messages will be classified in sections thanks to this. Section
## titles are the label, and a commit is classified under this section if any
## of the regexps associated is matching.
##
## Please note that ``section_regexps`` will only classify commits and won't
## make any changes to the contents. So you'll probably want to go check
## ``subject_process`` (or ``body_process``) to do some changes to the subject,
## whenever you are tweaking this variable.
##
section_regexps = [
('**New features**', [
r'^[aA]dded?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?(.*)$',
r'^[uU]pdated?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
r'^[cC]hanged?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
r'^[nN]ew?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
]),
('**Fixes**', [
r'^(?![mM]erge\s*)'
]
),
]
## ``body_process`` is a callable
##
## This callable will be given the original body and result will
## be used in the changelog.
##
## Available constructs are:
##
## - any python callable that take one txt argument and return txt argument.
##
## - ReSub(pattern, replacement): will apply regexp substitution.
##
## - Indent(chars=" "): will indent the text with the prefix
## Please remember that template engines gets also to modify the text and
## will usually indent themselves the text if needed.
##
## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns
##
## - noop: do nothing
##
## - ucfirst: ensure the first letter is uppercase.
## (usually used in the ``subject_process`` pipeline)
##
## - final_dot: ensure text finishes with a dot
## (usually used in the ``subject_process`` pipeline)
##
## - strip: remove any spaces before or after the content of the string
##
## - SetIfEmpty(msg="No commit message."): will set the text to
## whatever given ``msg`` if the current text is empty.
##
## Additionally, you can `pipe` the provided filters, for instance:
#body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ")
#body_process = Wrap(regexp=r'\n(?=\w+\s*:)')
#body_process = noop
body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip
## ``subject_process`` is a callable
##
## This callable will be given the original subject and result will
## be used in the changelog.
## subject_process = (strip |
## ReSub(r'^([aA]dd(ed?)?|[nN]ew)(\s?:?\s)(.*)$', r'![New](https://img.shields.io/badge/-- -New-brightgreen.svg?style=flat-square) \4') |
## ReSub(r'^([cC]hang(ed?)?)(\s?:?\s)(.*)$', r'![Changed](https://img.shields.io/badge/-- -Changed-orange.svg?style=flat-square) \4') |
## ReSub(r'^([fF]ix(ed?)?)(\s?:?\s)(.*)$', r'![Fixed](https://img.shields.io/badge/-- -Fixed-red.svg?style=flat-square) \4') |
## ReSub(r'^([uU]pdat(ed?)?)(\s?:?\s)(.*)$', r'![Updated](https://img.shields.io/badge/-- -Updated-blue.svg?style=flat-square) \4') |
## ReSub(r'#(\d{3,4})', r'[#\1](https://github.com/Radarr/Radarr/issues/\1)') |
## SetIfEmpty("No commit message.") | ucfirst | final_dot)
## Available constructs are those listed in ``body_process`` doc.
subject_process = (strip |
ReSub(r'^([aA]dd(ed?)?|[nN]ew)(\s?:?\s)(.*)$', r'![New](https://img.shields.io/badge/--%20-New-brightgreen.svg?style=flat-square) \4') |
ReSub(r'^([cC]hang(ed?)?)(\s?:?\s)(.*)$', r'![Changed](https://img.shields.io/badge/--%20-Changed-orange.svg?style=flat-square) \4') |
ReSub(r'^([fF]ix(ed?)?)(\s?:?\s)(.*)$', r'![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) \4') |
ReSub(r'^([uU]pdat(ed?)?)(\s?:?\s)(.*)$', r'![Updated](https://img.shields.io/badge/--%20-Updated-blue.svg?style=flat-square) \4') |
ReSub(r'#(\d{3,4})', r'[#\1](https://github.com/Radarr/Radarr/issues/\1)') |
SetIfEmpty("No commit message.") | ucfirst | final_dot)
## ``tag_filter_regexp`` is a regexp
##
## Tags that will be used for the changelog must match this regexp.
##
tag_filter_regexp = r'^v[0]+\.[2-9]+\.[0-9]+\.[0-9]{3,4}$'
## ``unreleased_version_label`` is a string or a callable that outputs a string
##
## This label will be used as the changelog Title of the last set of changes
## between last valid tag and HEAD if any.
unreleased_version_label = "(unreleased)"
## ``output_engine`` is a callable
##
## This will change the output format of the generated changelog file
##
## Available choices are:
##
## - rest_py
##
## Legacy pure python engine, outputs ReSTructured text.
## This is the default.
##
## - mustache(<template_name>)
##
## Template name could be any of the available templates in
## ``templates/mustache/*.tpl``.
## Requires python package ``pystache``.
## Examples:
## - mustache("markdown")
## - mustache("restructuredtext")
##
## - makotemplate(<template_name>)
##
## Template name could be any of the available templates in
## ``templates/mako/*.tpl``.
## Requires python package ``mako``.
## Examples:
## - makotemplate("restructuredtext")
##
#output_engine = rest_py
#output_engine = mustache("restructuredtext")
output_engine = mustache("changelog.tpl")
#output_engine = makotemplate("restructuredtext")
## ``include_merge`` is a boolean
##
## This option tells git-log whether to include merge commits in the log.
## The default is to include them.
include_merge = False
## ``log_encoding`` is a string identifier
##
## This option tells gitchangelog what encoding is outputed by ``git log``.
## The default is to be clever about it: it checks ``git config`` for
## ``i18n.logOutputEncoding``, and if not found will default to git's own
## default: ``utf-8``.
#log_encoding = 'utf-8'
## ``publish`` is a callable
##
## Sets what ``gitchangelog`` should do with the output generated by
## the output engine. ``publish`` is a callable taking one argument
## that is an interator on lines from the output engine.
##
## Some helper callable are provided:
##
## Available choices are:
##
## - stdout
##
## Outputs directly to standard output
## (This is the default)
##
## - FileInsertAtFirstRegexMatch(file, pattern, idx=lamda m: m.start())
##
## Creates a callable that will parse given file for the given
## regex pattern and will insert the output in the file.
## ``idx`` is a callable that receive the matching object and
## must return a integer index point where to insert the
## the output in the file. Default is to return the position of
## the start of the matched string.
##
## - FileRegexSubst(file, pattern, replace, flags)
##
## Apply a replace inplace in the given file. Your regex pattern must
## take care of everything and might be more complex. Check the README
## for a complete copy-pastable example.
##
# publish = FileInsertIntoFirstRegexMatch(
# "CHANGELOG.rst",
# r'/(?P<rev>[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n/',
# idx=lambda m: m.start(1)
# )
#publish = stdout
def write_to_file(content):
with open("CHANGELOG.md", "w+") as f:
for chunk in content:
f.write(chunk)
publish = write_to_file
## ``revs`` is a list of callable or a list of string
##
## callable will be called to resolve as strings and allow dynamical
## computation of these. The result will be used as revisions for
## gitchangelog (as if directly stated on the command line). This allows
## to filter exaclty which commits will be read by gitchangelog.
##
## To get a full documentation on the format of these strings, please
## refer to the ``git rev-list`` arguments. There are many examples.
##
## Using callables is especially useful, for instance, if you
## are using gitchangelog to generate incrementally your changelog.
##
## Some helpers are provided, you can use them::
##
## - FileFirstRegexMatch(file, pattern): will return a callable that will
## return the first string match for the given pattern in the given file.
## If you use named sub-patterns in your regex pattern, it'll output only
## the string matching the regex pattern named "rev".
##
## - Caret(rev): will return the rev prefixed by a "^", which is a
## way to remove the given revision and all its ancestor.
##
## Please note that if you provide a rev-list on the command line, it'll
## replace this value (which will then be ignored).
##
## If empty, then ``gitchangelog`` will act as it had to generate a full
## changelog.
##
## The default is to use all commits to make the changelog.
#revs = ["^1.0.3", ]
#revs = [
# Caret(
# FileFirstRegexMatch(
# "CHANGELOG.rst",
# r"(?P<rev>[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n")),
# "HEAD"
#]
revs = ["v0.2.0.134..."]
+311
View File
@@ -0,0 +1,311 @@
# -*- coding: utf-8; mode: python -*-
##
## Format
##
## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...]
##
## Description
##
## ACTION is one of 'chg', 'fix', 'new'
##
## Is WHAT the change is about.
##
## 'chg' is for refactor, small improvement, cosmetic changes...
## 'fix' is for bug fixes
## 'new' is for new features, big improvement
##
## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc'
##
## Is WHO is concerned by the change.
##
## 'dev' is for developpers (API changes, refactors...)
## 'usr' is for final users (UI changes)
## 'pkg' is for packagers (packaging changes)
## 'test' is for testers (test only related changes)
## 'doc' is for doc guys (doc only changes)
##
## COMMIT_MSG is ... well ... the commit message itself.
##
## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic'
##
## They are preceded with a '!' or a '@' (prefer the former, as the
## latter is wrongly interpreted in github.) Commonly used tags are:
##
## 'refactor' is obviously for refactoring code only
## 'minor' is for a very meaningless change (a typo, adding a comment)
## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...)
## 'wip' is for partial functionality but complete subfunctionality.
##
## Example:
##
## new: usr: support of bazaar implemented
## chg: re-indentend some lines !cosmetic
## new: dev: updated code to be compatible with last version of killer lib.
## fix: pkg: updated year of licence coverage.
## new: test: added a bunch of test around user usability of feature X.
## fix: typo in spelling my name in comment. !minor
##
## Please note that multi-line commit message are supported, and only the
## first line will be considered as the "summary" of the commit message. So
## tags, and other rules only applies to the summary. The body of the commit
## message will be displayed in the changelog without reformatting.
##
## ``ignore_regexps`` is a line of regexps
##
## Any commit having its full commit message matching any regexp listed here
## will be ignored and won't be reported in the changelog.
##
ignore_regexps = [
r'@minor', r'!minor',
r'@cosmetic', r'!cosmetic',
r'@refactor', r'!refactor',
r'@wip', r'!wip',
r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:',
r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:',
r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$',
r'^$', ## ignore commits with empty messages
]
## ``section_regexps`` is a list of 2-tuples associating a string label and a
## list of regexp
##
## Commit messages will be classified in sections thanks to this. Section
## titles are the label, and a commit is classified under this section if any
## of the regexps associated is matching.
##
## Please note that ``section_regexps`` will only classify commits and won't
## make any changes to the contents. So you'll probably want to go check
## ``subject_process`` (or ``body_process``) to do some changes to the subject,
## whenever you are tweaking this variable.
##
section_regexps = [
('**New features:**', [
r'^[aA]dded?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
r'^[uU]pdated?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
r'^[cC]hanged?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
r'^[nN]ew?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
]),
('**Fixes:**', [
r'^(?![mM]erge\s*)'
]
),
]
## ``body_process`` is a callable
##
## This callable will be given the original body and result will
## be used in the changelog.
##
## Available constructs are:
##
## - any python callable that take one txt argument and return txt argument.
##
## - ReSub(pattern, replacement): will apply regexp substitution.
##
## - Indent(chars=" "): will indent the text with the prefix
## Please remember that template engines gets also to modify the text and
## will usually indent themselves the text if needed.
##
## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns
##
## - noop: do nothing
##
## - ucfirst: ensure the first letter is uppercase.
## (usually used in the ``subject_process`` pipeline)
##
## - final_dot: ensure text finishes with a dot
## (usually used in the ``subject_process`` pipeline)
##
## - strip: remove any spaces before or after the content of the string
##
## - SetIfEmpty(msg="No commit message."): will set the text to
## whatever given ``msg`` if the current text is empty.
##
## Additionally, you can `pipe` the provided filters, for instance:
#body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ")
#body_process = Wrap(regexp=r'\n(?=\w+\s*:)')
#body_process = noop
body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip
## ``subject_process`` is a callable
##
## This callable will be given the original subject and result will
## be used in the changelog.
## subject_process = (strip |
## ReSub(r'^([aA]dd(ed?)?|[nN]ew)(\s?:?\s)(.*)$', r'![New](https://img.shields.io/badge/-- -New-brightgreen.svg?style=flat-square) \4') |
## ReSub(r'^([cC]hang(ed?)?)(\s?:?\s)(.*)$', r'![Changed](https://img.shields.io/badge/-- -Changed-orange.svg?style=flat-square) \4') |
## ReSub(r'^([fF]ix(ed?)?)(\s?:?\s)(.*)$', r'![Fixed](https://img.shields.io/badge/-- -Fixed-red.svg?style=flat-square) \4') |
## ReSub(r'^([uU]pdat(ed?)?)(\s?:?\s)(.*)$', r'![Updated](https://img.shields.io/badge/-- -Updated-blue.svg?style=flat-square) \4') |
## ReSub(r'#(\d{3,4})', r'[#\1](https://github.com/Radarr/Radarr/issues/\1)') |
## SetIfEmpty("No commit message.") | ucfirst | final_dot)
## Available constructs are those listed in ``body_process`` doc.
subject_process = (strip |
ReSub(r'^([aA]dd(ed?)?|[nN]ew)(\s?:?\s)(.*)$', r'\4') |
ReSub(r'^([cC]hang(ed?)?)(\s?:?\s)(.*)$', r'\4') |
ReSub(r'^([fF]ix(ed?)?)(\s?:?\s)(.*)$', r'\4') |
ReSub(r'^([uU]pdat(ed?)?)(\s?:?\s)(.*)$', r'\4') |
ReSub(r'#(\d{3,4})', r'Issue #\1') |
SetIfEmpty("No commit message.") | ucfirst | final_dot)
## ``tag_filter_regexp`` is a regexp
##
## Tags that will be used for the changelog must match this regexp.
##
tag_filter_regexp = r'^v[0]+\.[2-9]+\.[0-9]+\.[0-9]+$'
## ``unreleased_version_label`` is a string or a callable that outputs a string
##
## This label will be used as the changelog Title of the last set of changes
## between last valid tag and HEAD if any.
unreleased_version_label = "(unreleased)"
## ``output_engine`` is a callable
##
## This will change the output format of the generated changelog file
##
## Available choices are:
##
## - rest_py
##
## Legacy pure python engine, outputs ReSTructured text.
## This is the default.
##
## - mustache(<template_name>)
##
## Template name could be any of the available templates in
## ``templates/mustache/*.tpl``.
## Requires python package ``pystache``.
## Examples:
## - mustache("markdown")
## - mustache("restructuredtext")
##
## - makotemplate(<template_name>)
##
## Template name could be any of the available templates in
## ``templates/mako/*.tpl``.
## Requires python package ``mako``.
## Examples:
## - makotemplate("restructuredtext")
##
#output_engine = rest_py
#output_engine = mustache("restructuredtext")
output_engine = mustache("changelog_release.tpl")
#output_engine = makotemplate("restructuredtext")
## ``include_merge`` is a boolean
##
## This option tells git-log whether to include merge commits in the log.
## The default is to include them.
include_merge = False
## ``log_encoding`` is a string identifier
##
## This option tells gitchangelog what encoding is outputed by ``git log``.
## The default is to be clever about it: it checks ``git config`` for
## ``i18n.logOutputEncoding``, and if not found will default to git's own
## default: ``utf-8``.
#log_encoding = 'utf-8'
## ``publish`` is a callable
##
## Sets what ``gitchangelog`` should do with the output generated by
## the output engine. ``publish`` is a callable taking one argument
## that is an interator on lines from the output engine.
##
## Some helper callable are provided:
##
## Available choices are:
##
## - stdout
##
## Outputs directly to standard output
## (This is the default)
##
## - FileInsertAtFirstRegexMatch(file, pattern, idx=lamda m: m.start())
##
## Creates a callable that will parse given file for the given
## regex pattern and will insert the output in the file.
## ``idx`` is a callable that receive the matching object and
## must return a integer index point where to insert the
## the output in the file. Default is to return the position of
## the start of the matched string.
##
## - FileRegexSubst(file, pattern, replace, flags)
##
## Apply a replace inplace in the given file. Your regex pattern must
## take care of everything and might be more complex. Check the README
## for a complete copy-pastable example.
##
# publish = FileInsertIntoFirstRegexMatch(
# "CHANGELOG.rst",
# r'/(?P<rev>[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n/',
# idx=lambda m: m.start(1)
# )
publish = stdout
#def write_to_file(content):
# with open("CHANGELOG.md", "w+") as f:
# for chunk in content:
# f.write(chunk)
#publish = write_to_file
## ``revs`` is a list of callable or a list of string
##
## callable will be called to resolve as strings and allow dynamical
## computation of these. The result will be used as revisions for
## gitchangelog (as if directly stated on the command line). This allows
## to filter exaclty which commits will be read by gitchangelog.
##
## To get a full documentation on the format of these strings, please
## refer to the ``git rev-list`` arguments. There are many examples.
##
## Using callables is especially useful, for instance, if you
## are using gitchangelog to generate incrementally your changelog.
##
## Some helpers are provided, you can use them::
##
## - FileFirstRegexMatch(file, pattern): will return a callable that will
## return the first string match for the given pattern in the given file.
## If you use named sub-patterns in your regex pattern, it'll output only
## the string matching the regex pattern named "rev".
##
## - Caret(rev): will return the rev prefixed by a "^", which is a
## way to remove the given revision and all its ancestor.
##
## Please note that if you provide a rev-list on the command line, it'll
## replace this value (which will then be ignored).
##
## If empty, then ``gitchangelog`` will act as it had to generate a full
## changelog.
##
## The default is to use all commits to make the changelog.
#revs = ["^1.0.3", ]
#revs = [
# Caret(
# FileFirstRegexMatch(
# "CHANGELOG.rst",
# r"(?P<rev>[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n")),
# "HEAD"
#]
# Gets the latest annoted tag and uses that as a base for new changes.
import subprocess
proc = subprocess.Popen(["git", "describe", "--abbrev=0", "--tags"], stdout=subprocess.PIPE)
out = str(proc.communicate()[0].strip(), "utf-8")
revs = [out+"..."]
+22
View File
@@ -0,0 +1,22 @@
**Description:**
<!-- Check first that your problem is not listed in our wiki section:
* https://github.com/Radarr/Radarr/wiki/Common-Problems
* https://github.com/Radarr/Radarr/wiki/FAQ
**Just because you receive an exception in your logs, doesn't mean it's a bug and should be reported here. Often it's something else, such as a permission error. If you are unsure ask on the Discord or Subreddit first.**
Visit our [Discord server](https://discord.gg/NWYch8M) or [Subreddit](https://reddit.com/r/radarr) for support or longer discussions. Support questions posed on here will be closed immediately.
Provide a description of the feature request or bug here, the more details the better.
Please also include the following if you are reporting a bug. If you do not include it, the issue will probably be closed as we cannot help you. -->
**Radarr Version:**
**Mono Version:**
**Debug Logs:**
# Do not remove anything from your logs and post the full logs! If not everything fits in here use a service like https://pastebin.com to upload them.
<!-- Please use the search bar (make sure to include closed issues as well) and make sure you are not submitting an already submitted issue. -->
+34
View File
@@ -0,0 +1,34 @@
---
name: Bug report
about: Support requests will be closed immediately, if you are unsure go to our Discord
or Subreddit first. Exceptions do not mean you found a bug!
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Platform Information (please complete the following information):**
- OS: [e.g. Windows]
- Mono Version: [e.g. Mono 5.8] (Only needed under Linux and Mac, found under System -> Status)
- Browser and Version [e.g. chrome, safari] (Only needed for UI issues)
- Radarr Version [e.g. 3.0.0.2956]
**Debug Logs**
Turn on debug logs under Settings -> General and wait for the bug to occur again. **Upload the full log file here (or another site and link it). Issues will be closed, if they do not include this!**
-75
View File
@@ -1,75 +0,0 @@
name: Bug Report
title: "[BUG]: "
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
labels: ['Type: Bug', 'Status: Needs Triage']
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
validations:
required: false
- type: textarea
attributes:
label: Environment
description: |
examples:
- **OS**: Ubuntu 20.04
- **Radarr**: Radarr 3.0.1.4259
- **Docker Install**: Yes
- **Using Reverse Proxy**: No
- **Browser**: Firefox 90 (If UI related)
value: |
- OS:
- Radarr:
- Docker Install:
- Using Reverse Proxy:
- Browser:
render: markdown
validations:
required: true
- type: dropdown
attributes:
label: What branch are you running?
options:
- Master
- Develop
- Nightly
- Other (This issue will be closed)
validations:
required: true
- type: textarea
attributes:
label: Anything else?
description: |
Trace Logs (https://wiki.servarr.com/radarr/troubleshooting#logging-and-log-files)
Links? References? Anything that will give us more context about the issue you are encountering!
***Generally speaking, all bug reports must have trace logs provided.***
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: true
+1 -1
View File
@@ -1,7 +1,7 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Support via Discord - name: Support via Discord
url: https://radarr.video/discord url: https://discord.gg/AD3UP37
about: Chat with users and devs on support and setup related topics. about: Chat with users and devs on support and setup related topics.
- name: Support via Reddit - name: Support via Reddit
url: https://reddit.com/r/radarr url: https://reddit.com/r/radarr
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature request
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -1,39 +0,0 @@
name: Feature Request
title: "[FEAT]: "
description: 'Suggest an idea for Radarr'
labels: ['Type: Feature Request', 'Status: Needs Triage']
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the feature you are requesting.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe
description: A clear and concise description of what the problem is.
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Mockups? Anything that will give us more context about the feature you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: true
+3 -6
View File
@@ -1,16 +1,13 @@
#### Database Migration #### Database Migration
YES - XXXX | NO YES | NO
#### Description #### Description
A few sentences describing the overall goals of the pull request's commits.
#### Screenshot (if UI related)
#### Todos #### Todos
- [ ] Tests - [ ] Tests
- [ ] Translation Keys (./src/NzbDrone.Core/Localization/Core/en.json) - [ ] Translation Keys
- [ ] [Wiki Updates](https://wiki.servarr.com)
#### Issues Fixed or Closed by this PR #### Issues Fixed or Closed by this PR
* Fixes #XXXX * #
+2
View File
@@ -0,0 +1,2 @@
todo:
keyword: "TODO"
+1
View File
@@ -0,0 +1 @@
+4 -6
View File
@@ -4,12 +4,10 @@ daysUntilStale: 60
daysUntilClose: 7 daysUntilClose: 7
# Issues with these labels will never be considered stale # Issues with these labels will never be considered stale
exemptLabels: exemptLabels:
- feature request #legacy - feature request
- 'Type: Feature Request' - parser
- 'Status: Confirmed' - confirmed
- sonarr-pull - aphrodite
- lidarr-pull
- readarr-pull
# Label to use when marking an issue as stale # Label to use when marking an issue as stale
staleLabel: stale staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable # Comment to post when marking an issue as stale. Set to `false` to disable
+13
View File
@@ -0,0 +1,13 @@
# Configuration for support-requests - https://github.com/dessant/support-requests
# Label used to mark issues as support requests
supportLabel: support
# Comment to post on issues marked as support requests. Add a link
# to a support page, or set to `false` to disable
supportComment: >
We use the issue tracker exclusively for bug reports and feature requests.
However, this issue appears to be a support request. Please hop over onto our [Discord](https://discord.gg/ZDmT7qb) or [Subreddit](https://reddit.com/r/radarr)
# Whether to close issues marked as support requests
close: true
# Whether to lock issues marked as support requests
lock: false
-41
View File
@@ -1,41 +0,0 @@
name: Sync issue to Azure DevOps work item
on:
issues:
types:
[opened, edited, deleted, closed, reopened, labeled, unlabeled, assigned]
concurrency: azuresync-${{ github.event.issue.number }}
jobs:
alert:
runs-on: ubuntu-latest
steps:
- uses: danhellem/github-actions-issue-to-work-item@master
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == true }}"
env:
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
github_token: "${{ github.token }}"
ado_organization: "Servarr"
ado_project: "Servarr"
ado_area_path: "Servarr\\Radarr"
ado_wit: "Bug"
ado_new_state: "New"
ado_active_state: "Active"
ado_close_state: "Closed"
ado_bypassrules: true
log_level: 100
- uses: danhellem/github-actions-issue-to-work-item@master
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == false }}"
env:
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
github_token: "${{ github.token }}"
ado_organization: "Servarr"
ado_project: "Servarr"
ado_area_path: "Servarr\\Radarr"
ado_wit: "User Story"
ado_new_state: "New"
ado_active_state: "Active"
ado_close_state: "Closed"
ado_bypassrules: true
log_level: 100
-21
View File
@@ -1,21 +0,0 @@
name: 'Lock threads'
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: '90'
issue-exclude-created-before: ''
issue-exclude-labels: ''
issue-lock-labels: ''
issue-lock-comment: ''
issue-lock-reason: 'resolved'
process-only: ''
-21
View File
@@ -1,21 +0,0 @@
name: 'Support requests'
on:
issues:
types: [labeled, unlabeled, reopened]
jobs:
support:
runs-on: ubuntu-latest
steps:
- uses: dessant/support-requests@v2
with:
github-token: ${{ github.token }}
support-label: 'Type: Support'
issue-comment: >
:wave: @{issue-author}, we use the issue tracker exclusively
for bug reports and feature requests. However, this issue appears
to be a support request. Please hop over onto our [Discord](https://radarr.video/discord)
or [Subreddit](https://reddit.com/r/radarr)
close-issue: true
lock-issue: false
+1096
View File
File diff suppressed because it is too large Load Diff
+26 -6
View File
@@ -1,13 +1,33 @@
# How to Contribute # How to Contribute #
We're always looking for people to help make Radarr even better, there are a number of ways to contribute. We're always looking for people to help make Radarr even better, there are a number of ways to contribute.
This file has been moved to the wiki for the latest details please see the [contributing wiki page](https://wiki.servarr.com/radarr/contributing). ## Documentation ##
Setup guides, FAQ, the more information we have on the wiki the better.
## Documentation ## Development ##
Setup guides, [FAQ](https://wiki.servarr.com/radarr/faq), the more information we have on the [wiki](https://wiki.servarr.com/radarr) the better. See the readme for information on setting up your development environment.
## Development ### Contributing Code ###
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Radarr/Radarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)
- Rebase from Radarr's develop branch, don't merge
- Make meaningful commits, or squash them
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
- Reach out to us on the forums or on IRC if you have any questions
- Add tests (unit/integration)
- Commit with *nix line endings for consistency (We checkout Windows and commit *nix)
- One feature/bug fix per pull request to keep things clean and easy to understand
- Use 4 spaces instead of tabs, this is the default for VS 2012 and WebStorm (to my knowledge)
See the [Wiki Page](https://wiki.servarr.com/radarr/contributing) ### Pull Requesting ###
- Only make pull requests to develop, never master, if you make a PR to master we'll comment on it and close it
- You're probably going to get some comments or questions from us, they will be to ensure consistency and maintainability
- We'll try to respond to pull requests as soon as possible, if its been a day or two, please reach out to us, we may have missed it
- Each PR should come from its own [feature branch](http://martinfowler.com/bliki/FeatureBranch.html) not develop in your fork, it should have a meaningful branch name (what is being added/fixed)
- new-feature (Good)
- fix-bug (Good)
- patch (Bad)
- develop (Bad)
If you have any questions about any of this, please let us know.
+104 -31
View File
@@ -2,14 +2,62 @@
[![Build Status](https://dev.azure.com/Radarr/Radarr/_apis/build/status/Radarr.Radarr?branchName=develop)](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop) [![Build Status](https://dev.azure.com/Radarr/Radarr/_apis/build/status/Radarr.Radarr?branchName=develop)](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
[![Translated](https://translate.servarr.com/widgets/radarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget) [![Translated](https://translate.servarr.com/widgets/radarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/radarr/installation#docker) [![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://github.com/Radarr/Radarr/wiki/Docker)
![Github Downloads](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg) ![Github Downloads](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers) [![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Radarr/sponsors/badge.svg)](#sponsors)
[![Sponsors on Open Collective](https://opencollective.com/Radarr/sponsors/badge.svg)](#sponsors)
Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available. Radarr is an __independent__ fork of [Sonarr](https://github.com/Sonarr/Sonarr) reworked for automatically downloading movies via Usenet and BitTorrent.
## Major Features Include: The project was inspired by other Usenet/BitTorrent movie downloaders such as CouchPotato.
## Getting Started
[![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/wiki/Installation)
[![Docker](https://img.shields.io/badge/wiki-docker-1488C6.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/wiki/Docker)
[![Setup Guide](https://img.shields.io/badge/wiki-setup_guide-orange.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/wiki/Setup-Guide)
[![FAQ](https://img.shields.io/badge/wiki-FAQ-BF55EC.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/wiki/FAQ)
If you are using Docker please ensure your Docker paths are setup correctly using [this guide to facilitate](https://old.reddit.com/r/usenet/wiki/docker) hardlinks and minimize permissions issues.
* [Install Radarr for your desired OS](https://github.com/Radarr/Radarr/wiki/Installation) *or* use [Docker](https://github.com/Radarr/Radarr/wiki/Docker)
* *For Linux users*, run `radarr` and *optionally* have [Radarr start automatically](https://github.com/Radarr/Radarr/wiki/Autostart-on-Linux)
* Connect to the UI through <http://localhost:7878> or <http://your-ip:7878> in your web browser
* See the [Setup Guide](https://github.com/Radarr/Radarr/wiki/Setup-Guide) for further configuration
## Downloads
Please note that v0.2 will only have critical bugs resolved as of August 2020. Any additional development or features will be soley in V3.
| Release Type | Branch: develop (stable) | Branch: nightly (semi-unstable) | Branch: aphrodite (very-unstable) |
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Binary Releases | [![GitHub Releases](https://img.shields.io/badge/downloads-releases-brightgreen.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/releases) | [![AppVeyor Builds](https://img.shields.io/badge/downloads-nightly-green.svg?maxAge=60&style=flat-square)](https://ci.appveyor.com/project/galli-leo/radarr-usby1/branch/develop/artifacts) | |
| Docker - lsio | [![Docker release](https://img.shields.io/badge/linuxserver-radarr:latest-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/linuxserver/radarr) | [![Docker nightly](https://img.shields.io/badge/linuxserver-radarr:nightly-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/linuxserver/radarr) | [![Docker aphrodite](https://img.shields.io/badge/linuxserver-radarr:preview-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/linuxserver/radarr) |
| Docker - hotio | [![Docker release](https://img.shields.io/badge/hotio-radarr:latest-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/hotio/radarr) | [![Docker nightly](https://img.shields.io/badge/hotio-radarr:unstable-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/hotio/radarr) | [![Docker aphrodite](https://img.shields.io/badge/hotio-radarr:aphrodite-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/hotio/radarr) |
## Support
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60&style=flat-square)](https://discord.gg/AD3UP37)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60&style=flat-square)](https://www.reddit.com/r/radarr)
[![GitHub](https://img.shields.io/badge/github-issues-red.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/issues)
[![GitHub Wiki](https://img.shields.io/badge/github-wiki-181717.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/wiki)
## Status
[![GitHub issues](https://img.shields.io/github/issues/radarr/radarr.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/issues)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/radarr/radarr.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/pulls)
[![GNU GPL v3](https://img.shields.io/badge/license-GNU%20GPL%20v3-blue.svg?maxAge=60&style=flat-square)](http://www.gnu.org/licenses/gpl.html)
[![Copyright 2010-2020](https://img.shields.io/badge/copyright-2020-blue.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr)
[![Github Releases](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/releases/)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg?maxAge=60&style=flat-square)](https://hub.docker.com/r/linuxserver/radarr/)
[![Changelog](https://img.shields.io/github/commit-activity/w/radarr/radarr.svg?style=flat-square)](/CHANGELOG.md#unreleased)
### [Site and API Status](https://status.radarr.video)
Radarr is currently undergoing rapid development and pull requests are actively added into the repository.
## Features
### Current Features
* Adding new movies with lots of information, such as trailers, ratings, etc. * Adding new movies with lots of information, such as trailers, ratings, etc.
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc. * Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
@@ -21,53 +69,78 @@ Radarr is a movie collection manager for Usenet and BitTorrent users. It can mon
* Automatically importing downloaded movies * Automatically importing downloaded movies
* Recognizing Special Editions, Director's Cut, etc. * Recognizing Special Editions, Director's Cut, etc.
* Identifying releases with hardcoded subs * Identifying releases with hardcoded subs
* QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported * All indexers supported by Sonarr also supported
* New PassThePopcorn Indexer
* QBittorrent, Deluge, rTorrent, Transmission and uTorrent download client (Other clients are coming)
* New TorrentPotato Indexer
* Torznab Indexer now supports Movies (Works well with [Jackett](https://github.com/Jackett/Jackett))
* Scanning PreDB to know when a new release is available
* Importing movies from various online sources, such as IMDb Watchlists or Trakt (v3) (A complete list can be found [here](https://github.com/Radarr/Radarr/issues/114))
* Full integration with Kodi, Plex (notification, library update) * Full integration with Kodi, Plex (notification, library update)
* A beautiful UI * And a new beautiful UI (v3)
* Importing Metadata such as trailers or subtitles * Importing Metadata such as trailers or subtitles
* Adding metadata such as posters and information for Kodi and others to use * Adding metadata such as posters and information for Kodi and others to use
* Advanced customization for profiles, such that Radarr will always download the copy you want * Advanced customization for profiles, such that Radarr will always download the copy you want
## Support #### [Feature Requests](https://github.com/Radarr/Radarr/issues/new?assignees=&labels=feature+request&template=feature_request.md&title=)
Note: GitHub Issues are for Bugs and Feature Requests Only
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://radarr.video/discord) ## Configuring the Development Environment
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Radarr)
[![GitHub - Bugs and Feature Requests Only](https://img.shields.io/badge/github-issues-red.svg?maxAge=60)](https://github.com/Radarr/Radarr/issues)
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/radarr)
## Contributors & Developers ### Requirements
[API Documentation](https://radarr.video/docs/api/)
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md). * [Visual Studio Community 2019](https://www.visualstudio.com/vs/community/) or [Rider](http://www.jetbrains.com/rider/)
<a href="https://github.com/Radarr/Radarr/graphs/contributors"><img src="https://opencollective.com/Radarr/contributors.svg?width=890&button=false" /></a> * [Git](https://git-scm.com/downloads)
* [Node.js](https://nodejs.org/en/download/)
* [Yarn](https://yarnpkg.com/)
### Setup
## Backers * Make sure all the required software mentioned above are installed
* Clone the repository into your development machine ([*info*](https://help.github.com/desktop/guides/contributing/working-with-your-remote-repository-on-github-or-github-enterprise))
* Install the required Node Packages `yarn install`
* Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/Radarr#backer) > **Notice**
> Gulp must be running at all times while you are working with Radarr client source files.
<img src="https://opencollective.com/Radarr/backers.svg?width=890"></a> ### Build
## Sponsors * To build run `sh build.sh`
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/Radarr#sponsor) **Note:** Windows users must have bash available to do this. If you installed git, you should have a git bash utility that works.
<img src="https://opencollective.com/Radarr/sponsors.svg?width=890"></a> ### Development
## Mega Sponsors * Open `Radarr.sln` in Visual Studio 2019 or run the build.sh script, if Mono is installed. Alternatively you can use Jetbrains Rider, since it works on all Platforms.
* Make sure `NzbDrone.Console` is set as the startup project
* Run `build.sh` before running, or build in VS
<img src="https://opencollective.com/Radarr/tiers/mega-sponsor.svg?width=890"></a> ## Supporters
## JetBrains This project would not be possible without the support by these amazing folks. [**Become a sponsor or backer**](https://opencollective.com/radarr) to help us out!
Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
### Sponsors
[![Sponsors](https://opencollective.com/radarr/tiers/sponsor.svg)](https://opencollective.com/radarr/order/3851)
### Flexible Sponsors
[![Flexible Sponsors](https://opencollective.com/radarr/tiers/flexible-sponsor.svg?avatarHeight=54)](https://opencollective.com/radarr/order/3856)
### Backers
[![Backers](https://opencollective.com/radarr/tiers/backer.svg?avatarHeight=48)](https://opencollective.com/radarr/order/3850)
### JetBrains
Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
* [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/) * [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/) * [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/) * [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
* [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/) * [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
### License ## License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) * [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2021 * Copyright 2010-2020
+117 -168
View File
@@ -7,28 +7,24 @@ variables:
outputFolder: './_output' outputFolder: './_output'
artifactsFolder: './_artifacts' artifactsFolder: './_artifacts'
testsFolder: './_tests' testsFolder: './_tests'
majorVersion: '4.0.0' majorVersion: '3.0.0'
minorVersion: $[counter('minorVersion', 2000)] minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)' radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)' buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '5.0.401' dotnetVersion: '3.1.401'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
trigger: trigger:
branches: branches:
include: include:
- develop - develop
- master - aphrodite
pr: pr:
branches: - develop
include: - aphrodite
- develop
paths:
exclude:
- src/NzbDrone.Core/Localization/Core
stages: stages:
- stage: Setup - stage: Setup
@@ -44,7 +40,7 @@ stages:
displayName: Set Build Name displayName: Set Build Name
- bash: | - bash: |
if [[ $BUILD_REASON == "PullRequest" ]]; then if [[ $BUILD_REASON == "PullRequest" ]]; then
git diff origin/develop...HEAD --name-only | grep -E "^(src/|azure-pipelines.yml)" git diff origin/aphrodite...HEAD --name-only | grep -E "^(src/|azure-pipelines.yml)"
echo $? > not_backend_update echo $? > not_backend_update
else else
echo 0 > not_backend_update echo 0 > not_backend_update
@@ -64,21 +60,18 @@ stages:
Linux: Linux:
osName: 'Linux' osName: 'Linux'
imageName: 'ubuntu-18.04' imageName: 'ubuntu-18.04'
enableAnalysis: 'true'
Mac: Mac:
osName: 'Mac' osName: 'Mac'
imageName: 'macos-10.14' imageName: 'macos-10.14'
enableAnalysis: 'false'
Windows: Windows:
osName: 'Windows' osName: 'Windows'
imageName: 'windows-2019' imageName: 'windows-2019'
enableAnalysis: 'false'
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
variables: variables:
# Disable stylecop here - linting errors get caught by the analyze task # Disable stylecop here - linting errors get caught by the analyze task
EnableAnalyzers: $(enableAnalysis) EnableAnalyzers: 'false'
steps: steps:
- checkout: self - checkout: self
submodules: true submodules: true
@@ -87,18 +80,7 @@ stages:
displayName: 'Install .net core' displayName: 'Install .net core'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
- bash: | - bash: ./build.sh --backend
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
echo $BUNDLEDVERSIONS
grep osx-x64 $BUNDLEDVERSIONS
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "BSD already enabled"
else
echo "Enabling BSD support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
fi
displayName: Enable FreeBSD Support
- bash: ./build.sh --backend --enable-bsd
displayName: Build Radarr Backend displayName: Build Radarr Backend
- bash: | - bash: |
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \; find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
@@ -111,23 +93,23 @@ stages:
artifact: '$(osName)Backend' artifact: '$(osName)Backend'
displayName: Publish Backend displayName: Publish Backend
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net5.0/win-x64/publish' - publish: '$(testsFolder)/netcoreapp3.1/win-x64/publish'
artifact: WindowsCoreTests artifact: WindowsCoreTests
displayName: Publish Windows Test Package displayName: Publish Windows Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net5.0/linux-x64/publish' - publish: '$(testsFolder)/net462/linux-x64/publish'
artifact: LinuxTests
displayName: Publish Linux Mono Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/netcoreapp3.1/linux-x64/publish'
artifact: LinuxCoreTests artifact: LinuxCoreTests
displayName: Publish Linux Test Package displayName: Publish Linux Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net5.0/linux-musl-x64/publish' - publish: '$(testsFolder)/netcoreapp3.1/linux-musl-x64/publish'
artifact: LinuxMuslCoreTests artifact: LinuxMuslCoreTests
displayName: Publish Linux Musl Test Package displayName: Publish Linux Musl Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net5.0/freebsd-x64/publish' - publish: '$(testsFolder)/netcoreapp3.1/osx-x64/publish'
artifact: FreebsdCoreTests
displayName: Publish FreeBSD Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net5.0/osx-x64/publish'
artifact: MacCoreTests artifact: MacCoreTests
displayName: Publish MacOS Test Package displayName: Publish MacOS Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
@@ -154,7 +136,7 @@ stages:
- task: NodeTool@0 - task: NodeTool@0
displayName: Set Node.js version displayName: Set Node.js version
inputs: inputs:
versionSpec: '12.x' versionSpec: '10.x'
- checkout: self - checkout: self
submodules: true submodules: true
fetchDepth: 1 fetchDepth: 1
@@ -203,12 +185,8 @@ stages:
- bash: ./build.sh --packages - bash: ./build.sh --packages
displayName: Create Packages displayName: Create Packages
- bash: | - bash: |
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net5.0 //DRuntime=win-x86 setup/inno/ISCC.exe setup/radarr.iss //DFramework=netcoreapp3.1
cp setup/output/Radarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x86-installer.exe cp setup/output/Radarr.*windows.netcoreapp3.1.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x64-installer.exe
displayName: Create .NET Core Windows installer
- bash: |
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net5.0 //DRuntime=win-x64
cp setup/output/Radarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x64-installer.exe
displayName: Create .NET Core Windows installer displayName: Create .NET Core Windows installer
- publish: $(Build.ArtifactStagingDirectory) - publish: $(Build.ArtifactStagingDirectory)
artifact: 'WindowsInstaller' artifact: 'WindowsInstaller'
@@ -238,7 +216,7 @@ stages:
artifactName: WindowsFrontend artifactName: WindowsFrontend
targetPath: _output targetPath: _output
displayName: Fetch Frontend displayName: Fetch Frontend
- bash: ./build.sh --packages --enable-bsd - bash: ./build.sh --packages
displayName: Create Packages displayName: Create Packages
- bash: | - bash: |
find . -name "Radarr" -exec chmod a+x {} \; find . -name "Radarr" -exec chmod a+x {} \;
@@ -250,21 +228,14 @@ stages:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x64/net5.0 rootFolderOrFile: $(artifactsFolder)/windows/netcoreapp3.1
- task: ArchiveFiles@2
displayName: Create Windows x86 Core zip
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x86.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x86/net5.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS Core app displayName: Create MacOS Core app
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-x64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-x64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/macos-app/net5.0 rootFolderOrFile: $(artifactsFolder)/macos-app/netcoreapp3.1
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create MacOS Core tar displayName: Create MacOS Core tar
inputs: inputs:
@@ -272,7 +243,15 @@ stages:
archiveType: 'tar' archiveType: 'tar'
tarCompression: 'gz' tarCompression: 'gz'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/macos/net5.0 rootFolderOrFile: $(artifactsFolder)/macos/netcoreapp3.1
- task: ArchiveFiles@2
displayName: Create Linux Mono tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x64/net462
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Linux Core tar displayName: Create Linux Core tar
inputs: inputs:
@@ -280,7 +259,7 @@ stages:
archiveType: 'tar' archiveType: 'tar'
tarCompression: 'gz' tarCompression: 'gz'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x64/net5.0 rootFolderOrFile: $(artifactsFolder)/linux-x64/netcoreapp3.1
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create Linux Musl Core tar displayName: Create Linux Musl Core tar
inputs: inputs:
@@ -288,7 +267,7 @@ stages:
archiveType: 'tar' archiveType: 'tar'
tarCompression: 'gz' tarCompression: 'gz'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net5.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/netcoreapp3.1
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM32 Linux Core tar displayName: Create ARM32 Linux Core tar
inputs: inputs:
@@ -296,7 +275,7 @@ stages:
archiveType: 'tar' archiveType: 'tar'
tarCompression: 'gz' tarCompression: 'gz'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm/net5.0 rootFolderOrFile: $(artifactsFolder)/linux-arm/netcoreapp3.1
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM64 Linux Core tar displayName: Create ARM64 Linux Core tar
inputs: inputs:
@@ -304,7 +283,7 @@ stages:
archiveType: 'tar' archiveType: 'tar'
tarCompression: 'gz' tarCompression: 'gz'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net5.0 rootFolderOrFile: $(artifactsFolder)/linux-arm64/netcoreapp3.1
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create ARM64 Linux Musl Core tar displayName: Create ARM64 Linux Musl Core tar
inputs: inputs:
@@ -312,15 +291,7 @@ stages:
archiveType: 'tar' archiveType: 'tar'
tarCompression: 'gz' tarCompression: 'gz'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net5.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/netcoreapp3.1
- task: ArchiveFiles@2
displayName: Create FreeBSD Core Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).freebsd-core-x64.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net5.0
- publish: $(Build.ArtifactStagingDirectory) - publish: $(Build.ArtifactStagingDirectory)
artifact: 'Packages' artifact: 'Packages'
displayName: Publish Packages displayName: Publish Packages
@@ -331,22 +302,14 @@ stages:
sentry-cli releases new --finalize -p radarr -p radarr-ui -p radarr-update "${RELEASENAME}" sentry-cli releases new --finalize -p radarr -p radarr-ui -p radarr-update "${RELEASENAME}"
sentry-cli releases -p radarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite sentry-cli releases -p radarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite
sentry-cli releases set-commits --auto "${RELEASENAME}" sentry-cli releases set-commits --auto "${RELEASENAME}"
if [[ ${BUILD_SOURCEBRANCH} == "refs/heads/develop" ]]; then sentry-cli releases deploys "${RELEASENAME}" new -e aphrodite
sentry-cli releases deploys "${RELEASENAME}" new -e nightly
else
sentry-cli releases deploys "${RELEASENAME}" new -e production
fi
if [ $? -gt 0 ]; then if [ $? -gt 0 ]; then
echo "##vso[task.logissue type=warning]Error uploading source maps." echo "##vso[task.logissue type=warning]Error uploading source maps."
fi fi
exit 0 exit 0
displayName: Publish Sentry Source Maps displayName: Publish Sentry Source Maps
condition: | continueOnError: true
or condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/aphrodite'))
(
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop')),
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
)
env: env:
SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr) SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr)
SENTRY_ORG: $(sentryOrg) SENTRY_ORG: $(sentryOrg)
@@ -374,34 +337,24 @@ stages:
displayName: Unit Native displayName: Unit Native
dependsOn: Prepare dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
workspace:
clean: all
strategy: strategy:
matrix: matrix:
MacCore: MacCore:
osName: 'Mac' osName: 'Mac'
testName: 'MacCore' testName: 'MacCore'
poolName: 'Azure Pipelines'
imageName: 'macos-10.14' imageName: 'macos-10.14'
WindowsCore: WindowsCore:
osName: 'Windows' osName: 'Windows'
testName: 'WindowsCore' testName: 'WindowsCore'
poolName: 'Azure Pipelines'
imageName: 'windows-2019' imageName: 'windows-2019'
LinuxCore: LinuxCore:
osName: 'Linux' osName: 'Linux'
testName: 'LinuxCore' testName: 'LinuxCore'
poolName: 'Azure Pipelines'
imageName: 'ubuntu-18.04' imageName: 'ubuntu-18.04'
FreebsdCore: pattern: 'Radarr.**.linux-core-x64.tar.gz'
osName: 'Linux'
testName: 'FreebsdCore'
poolName: 'FreeBSD'
imageName:
pool: pool:
name: $(poolName)
vmImage: $(imageName) vmImage: $(imageName)
steps: steps:
@@ -410,7 +363,6 @@ stages:
displayName: 'Install .net core' displayName: 'Install .net core'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
condition: ne(variables['poolName'], 'FreeBSD')
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
@@ -423,10 +375,23 @@ stages:
sudo apt-get update sudo apt-get update
sudo apt-get install -y --allow-unauthenticated libmediainfo-dev libmediainfo0v5 mediainfo sudo apt-get install -y --allow-unauthenticated libmediainfo-dev libmediainfo0v5 mediainfo
displayName: Install mediainfo displayName: Install mediainfo
condition: and(succeeded(), eq(variables['testName'], 'LinuxCore')) condition: and(succeeded(), eq(variables['osName'], 'Linux'))
- powershell: Set-Service SCardSvr -StartupType Manual - powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service displayName: Enable Windows Test Service
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- bash: |
wget https://github.com/acoustid/chromaprint/releases/download/v1.4.3/chromaprint-fpcalc-1.4.3-linux-x86_64.tar.gz
sudo tar xf chromaprint-fpcalc-1.4.3-linux-x86_64.tar.gz --strip-components=1 --directory /usr/bin
displayName: Install fpcalc
condition: and(succeeded(), eq(variables['osName'], 'Linux'))
- bash: |
SYMLINK=6_6_0
MONOPREFIX=/Library/Frameworks/Mono.framework/Versions/$SYMLINK
echo "##vso[task.setvariable variable=MONOPREFIX;]$MONOPREFIX"
echo "##vso[task.setvariable variable=PKG_CONFIG_PATH;]$MONOPREFIX/lib/pkgconfig:$MONOPREFIX/share/pkgconfig:$PKG_CONFIG_PATH"
echo "##vso[task.setvariable variable=PATH;]$MONOPREFIX/bin:$PATH"
displayName: Set Mono Version
condition: and(succeeded(), eq(variables['osName'], 'Mac'))
- bash: find ${TESTSFOLDER} -name "Radarr.Test.Dummy" -exec chmod a+x {} \; - bash: find ${TESTSFOLDER} -name "Radarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows')) condition: and(succeeded(), ne(variables['osName'], 'Windows'))
@@ -450,10 +415,22 @@ stages:
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
strategy: strategy:
matrix: matrix:
mono520:
testName: 'Mono 5.20'
artifactName: LinuxTests
containerImage: servarr/testimages:mono-5.20
mono610:
testName: 'Mono 6.10'
artifactName: LinuxTests
containerImage: servarr/testimages:mono-6.10
mono612:
testName: 'Mono 6.12'
artifactName: LinuxTests
containerImage: servarr/testimages:mono-6.12
alpine: alpine:
testName: 'Musl Net Core' testName: 'Musl Net Core'
artifactName: LinuxMuslCoreTests artifactName: LinuxMuslCoreTests
containerImage: ghcr.io/servarr/testimages:alpine containerImage: servarr/testimages:alpine
pool: pool:
vmImage: 'ubuntu-18.04' vmImage: 'ubuntu-18.04'
@@ -518,22 +495,30 @@ stages:
osName: 'Mac' osName: 'Mac'
testName: 'MacCore' testName: 'MacCore'
imageName: 'macos-10.14' imageName: 'macos-10.14'
pattern: 'Radarr.*.osx-core-x64.tar.gz' pattern: 'Radarr.**.osx-core-x64.tar.gz'
WindowsCore: WindowsCore:
osName: 'Windows' osName: 'Windows'
testName: 'WindowsCore' testName: 'WindowsCore'
imageName: 'windows-2019' imageName: 'windows-2019'
pattern: 'Radarr.*.windows-core-x64.zip' pattern: 'Radarr.**.windows-core-x64.zip'
LinuxCore: LinuxCore:
osName: 'Linux' osName: 'Linux'
testName: 'LinuxCore' testName: 'LinuxCore'
imageName: 'ubuntu-18.04' imageName: 'ubuntu-18.04'
pattern: 'Radarr.*.linux-core-x64.tar.gz' pattern: 'Radarr.**.linux-core-x64.tar.gz'
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
steps: steps:
- bash: |
SYMLINK=6_6_0
MONOPREFIX=/Library/Frameworks/Mono.framework/Versions/$SYMLINK
echo "##vso[task.setvariable variable=MONOPREFIX;]$MONOPREFIX"
echo "##vso[task.setvariable variable=PKG_CONFIG_PATH;]$MONOPREFIX/lib/pkgconfig:$MONOPREFIX/share/pkgconfig:$PKG_CONFIG_PATH"
echo "##vso[task.setvariable variable=PATH;]$MONOPREFIX/bin:$PATH"
displayName: Set Mono Version
condition: and(succeeded(), eq(variables['osName'], 'Mac'))
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .net core'
inputs: inputs:
@@ -573,63 +558,32 @@ stages:
failTaskOnFailedTests: true failTaskOnFailedTests: true
displayName: Publish Test Results displayName: Publish Test Results
- job: Integration_FreeBSD
displayName: Integration Native FreeBSD
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
workspace:
clean: all
variables:
pattern: 'Radarr.*.freebsd-core-x64.tar.gz'
pool:
name: 'FreeBSD'
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'FreebsdCoreTests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
buildType: 'current'
artifactName: Packages
itemPattern: '/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory)
- bash: |
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
tar xf ${BUILD_ARTIFACTSTAGINGDIRECTORY}/$(pattern) -C ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
displayName: Extract Package
- bash: |
mkdir -p ./bin/
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Radarr/. ./bin/
displayName: Move Package Contents
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh Linux Integration Test
displayName: Run Integration Tests
- task: PublishTestResults@2
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'FreeBSD Integration Tests'
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_Docker - job: Integration_Docker
displayName: Integration Docker displayName: Integration Docker
dependsOn: Prepare dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
strategy: strategy:
matrix: matrix:
mono520:
testName: 'Mono 5.20'
artifactName: LinuxTests
containerImage: servarr/testimages:mono-5.20
pattern: 'Radarr.**.linux.tar.gz'
mono610:
testName: 'Mono 6.10'
artifactName: LinuxTests
containerImage: servarr/testimages:mono-6.10
pattern: 'Radarr.**.linux.tar.gz'
mono612:
testName: 'Mono 6.12'
artifactName: LinuxTests
containerImage: servarr/testimages:mono-6.12
pattern: 'Radarr.**.linux.tar.gz'
alpine: alpine:
testName: 'Musl Net Core' testName: 'Musl Net Core'
artifactName: LinuxMuslCoreTests artifactName: LinuxMuslCoreTests
containerImage: ghcr.io/servarr/testimages:alpine containerImage: servarr/testimages:alpine
pattern: 'Radarr.*.linux-musl-core-x64.tar.gz' pattern: 'Radarr.**.linux-musl-core-x64.tar.gz'
pool: pool:
vmImage: 'ubuntu-18.04' vmImage: 'ubuntu-18.04'
@@ -688,17 +642,17 @@ stages:
Linux: Linux:
osName: 'Linux' osName: 'Linux'
imageName: 'ubuntu-18.04' imageName: 'ubuntu-18.04'
pattern: 'Radarr.*.linux-core-x64.tar.gz' pattern: 'Radarr.**.linux-core-x64.tar.gz'
failBuild: true failBuild: true
Mac: Mac:
osName: 'Mac' osName: 'Mac'
imageName: 'macos-10.14' imageName: 'macos-10.14'
pattern: 'Radarr.*.osx-core-x64.tar.gz' pattern: 'Radarr.**.osx-core-x64.tar.gz'
failBuild: true failBuild: true
Windows: Windows:
osName: 'Windows' osName: 'Windows'
imageName: 'windows-2019' imageName: 'windows-2019'
pattern: 'Radarr.*.windows-core-x64.zip' pattern: 'Radarr.**.windows-core-x64.zip'
failBuild: true failBuild: true
pool: pool:
@@ -732,21 +686,24 @@ stages:
mkdir -p ./bin/ mkdir -p ./bin/
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Radarr/. ./bin/ cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Radarr/. ./bin/
displayName: Move Package Contents displayName: Move Package Contents
- bash: |
if [[ $OSNAME == "Mac" ]]; then
url=https://github.com/mozilla/geckodriver/releases/download/v0.27.0/geckodriver-v0.27.0-macos.tar.gz
elif [[ $OSNAME == "Linux" ]]; then
url=https://github.com/mozilla/geckodriver/releases/download/v0.27.0/geckodriver-v0.27.0-linux64.tar.gz
else
echo "Unhandled OS"
exit 1
fi
curl -s -L "$url" | tar -xz
chmod +x geckodriver
mv geckodriver _tests
displayName: Install Gecko Driver
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- bash: | - bash: |
chmod a+x ${TESTSFOLDER}/test.sh chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh ${OSNAME} Automation Test ${TESTSFOLDER}/test.sh ${OSNAME} Automation Test
displayName: Run Automation Tests displayName: Run Automation Tests
- task: CopyFiles@2
displayName: 'Copy Screenshot to: $(Build.ArtifactStagingDirectory)'
inputs:
SourceFolder: '$(Build.SourcesDirectory)'
Contents: |
**/*_test_screenshot.png
TargetFolder: '$(Build.ArtifactStagingDirectory)/screenshots'
- publish: $(Build.ArtifactStagingDirectory)/screenshots
artifact: '$(osName)AutomationScreenshots'
displayName: Publish Screenshot Bundle
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
- task: PublishTestResults@2 - task: PublishTestResults@2
inputs: inputs:
testResultsFormat: 'NUnit' testResultsFormat: 'NUnit'
@@ -790,7 +747,7 @@ stages:
- task: NodeTool@0 - task: NodeTool@0
displayName: Set Node.js version displayName: Set Node.js version
inputs: inputs:
versionSpec: '12.x' versionSpec: '10.x'
- checkout: self - checkout: self
submodules: true submodules: true
fetchDepth: 1 fetchDepth: 1
@@ -836,7 +793,6 @@ stages:
variables: variables:
disable.coverage.autogenerate: 'true' disable.coverage.autogenerate: 'true'
EnableAnalyzers: 'false'
pool: pool:
vmImage: windows-2019 vmImage: windows-2019
@@ -861,12 +817,12 @@ stages:
projectVersion: '$(radarrVersion)' projectVersion: '$(radarrVersion)'
extraProperties: | extraProperties: |
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/** sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**
sonar.coverage.exclusions=**/Radarr.Api.V3/**/* sonar.coverage.exclusions=**/Radarr.Api.V3/**/*,**/NzbDrone.Api/**/*,**/MonoTorrent/**/*,**/Marr.Data/**/*
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
- bash: | - bash: |
./build.sh --backend -f net5.0 -r win-x64 ./build.sh --backend -f netcoreapp3.1 -r win-x64
TEST_DIR=_tests/net5.0/win-x64/publish/ ./test.sh Windows Unit Coverage TEST_DIR=_tests/netcoreapp3.1/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests displayName: Coverage Unit Tests
- task: SonarCloudAnalyze@1 - task: SonarCloudAnalyze@1
condition: eq(variables['System.PullRequest.IsFork'], 'False') condition: eq(variables['System.PullRequest.IsFork'], 'False')
@@ -898,15 +854,8 @@ stages:
pool: pool:
vmImage: 'ubuntu-18.04' vmImage: 'ubuntu-18.04'
steps: steps:
- task: DownloadPipelineArtifact@2
continueOnError: true
displayName: Download Screenshot Artifact
inputs:
buildType: 'current'
artifactName: 'WindowsAutomationScreenshots'
targetPath: $(Build.SourcesDirectory)
- checkout: none - checkout: none
- pwsh: | - powershell: |
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/Servarr/AzureDiscordNotify/master/DiscordNotify.ps1')) iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/Servarr/AzureDiscordNotify/master/DiscordNotify.ps1'))
env: env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken) SYSTEM_ACCESSTOKEN: $(System.AccessToken)
+38 -55
View File
@@ -1,4 +1,4 @@
#! /usr/bin/env bash #! /bin/bash
set -e set -e
outputFolder='_output' outputFolder='_output'
@@ -25,17 +25,6 @@ UpdateVersionNumber()
fi fi
} }
EnableBsdSupport()
{
#todo enable sdk with
#SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
# BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
if grep -qv freebsd-x64 src/Directory.Build.props; then
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props
fi
}
LintUI() LintUI()
{ {
ProgressStart 'ESLint' ProgressStart 'ESLint'
@@ -86,11 +75,11 @@ YarnInstall()
ProgressEnd 'yarn install' ProgressEnd 'yarn install'
} }
RunWebpack() RunGulp()
{ {
ProgressStart 'Running webpack' ProgressStart 'Running gulp'
yarn run build --env production yarn run build --production
ProgressEnd 'Running webpack' ProgressEnd 'Running gulp'
} }
PackageFiles() PackageFiles()
@@ -129,7 +118,7 @@ PackageLinux()
echo "Adding Radarr.Mono to UpdatePackage" echo "Adding Radarr.Mono to UpdatePackage"
cp $folder/Radarr.Mono.* $folder/Radarr.Update cp $folder/Radarr.Mono.* $folder/Radarr.Update
if [ "$framework" = "net5.0" ]; then if [ "$framework" = "netcoreapp3.1" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
fi fi
@@ -147,6 +136,11 @@ PackageMacOS()
PackageFiles "$folder" "$framework" "osx-x64" PackageFiles "$folder" "$framework" "osx-x64"
if [ "$framework" = "net462" ]; then
echo "Adding Startup script"
cp macOS/Radarr $folder
fi
echo "Removing Service helpers" echo "Removing Service helpers"
rm -f $folder/ServiceUninstall.* rm -f $folder/ServiceUninstall.*
rm -f $folder/ServiceInstall.* rm -f $folder/ServiceInstall.*
@@ -156,7 +150,7 @@ PackageMacOS()
echo "Adding Radarr.Mono to UpdatePackage" echo "Adding Radarr.Mono to UpdatePackage"
cp $folder/Radarr.Mono.* $folder/Radarr.Update cp $folder/Radarr.Mono.* $folder/Radarr.Update
if [ "$framework" = "net5.0" ]; then if [ "$framework" = "netcoreapp3.1" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
fi fi
@@ -189,14 +183,12 @@ PackageMacOSApp()
PackageWindows() PackageWindows()
{ {
local framework="$1" local framework="$1"
local runtime="$2"
ProgressStart "Creating Windows Package for $framework" ProgressStart "Creating Windows Package for $framework"
local folder=$artifactsFolder/$runtime/$framework/Radarr local folder=$artifactsFolder/windows/$framework/Radarr
PackageFiles "$folder" "$framework" "$runtime" PackageFiles "$folder" "$framework" "win-x64"
cp -r $outputFolder/$framework-windows/$runtime/publish/* $folder
echo "Removing Radarr.Mono" echo "Removing Radarr.Mono"
rm -f $folder/Radarr.Mono.* rm -f $folder/Radarr.Mono.*
@@ -218,11 +210,11 @@ Package()
IFS='-' read -ra SPLIT <<< "$runtime" IFS='-' read -ra SPLIT <<< "$runtime"
case "${SPLIT[0]}" in case "${SPLIT[0]}" in
linux|freebsd*) linux)
PackageLinux "$framework" "$runtime" PackageLinux "$framework" "$runtime"
;; ;;
win) win)
PackageWindows "$framework" "$runtime" PackageWindows "$framework"
;; ;;
osx) osx)
PackageMacOS "$framework" PackageMacOS "$framework"
@@ -240,6 +232,14 @@ PackageTests()
rm -f $testPackageFolder/$framework/$runtime/*.log.config rm -f $testPackageFolder/$framework/$runtime/*.log.config
# geckodriver.exe isn't copied by dotnet publish
if [ "$runtime" = "win-x64" ];
then
curl -Lso gecko.zip "https://github.com/mozilla/geckodriver/releases/download/v0.27.0/geckodriver-v0.27.0-win64.zip"
unzip -o gecko.zip
cp geckodriver.exe "$testPackageFolder/$framework/win-x64/publish"
fi
ProgressEnd 'Creating Test Package' ProgressEnd 'Creating Test Package'
} }
@@ -263,7 +263,6 @@ if [ $# -eq 0 ]; then
FRONTEND=YES FRONTEND=YES
PACKAGES=YES PACKAGES=YES
LINT=YES LINT=YES
ENABLE_BSD=NO
fi fi
while [[ $# -gt 0 ]] while [[ $# -gt 0 ]]
@@ -275,10 +274,6 @@ case $key in
BACKEND=YES BACKEND=YES
shift # past argument shift # past argument
;; ;;
--enable-bsd)
ENABLE_BSD=YES
shift # past argument
;;
-r|--runtime) -r|--runtime)
RID="$2" RID="$2"
shift # past argument shift # past argument
@@ -319,22 +314,14 @@ set -- "${POSITIONAL[@]}" # restore positional parameters
if [ "$BACKEND" = "YES" ]; if [ "$BACKEND" = "YES" ];
then then
UpdateVersionNumber UpdateVersionNumber
if [ "$ENABLE_BSD" = "YES" ];
then
EnableBsdSupport
fi
Build Build
if [[ -z "$RID" || -z "$FRAMEWORK" ]]; if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then then
PackageTests "net5.0" "win-x64" PackageTests "netcoreapp3.1" "win-x64"
PackageTests "net5.0" "win-x86" PackageTests "netcoreapp3.1" "linux-x64"
PackageTests "net5.0" "linux-x64" PackageTests "netcoreapp3.1" "linux-musl-x64"
PackageTests "net5.0" "linux-musl-x64" PackageTests "netcoreapp3.1" "osx-x64"
PackageTests "net5.0" "osx-x64" PackageTests "net462" "linux-x64"
if [ "$ENABLE_BSD" = "YES" ];
then
PackageTests "net5.0" "freebsd-x64"
fi
else else
PackageTests "$FRAMEWORK" "$RID" PackageTests "$FRAMEWORK" "$RID"
fi fi
@@ -343,7 +330,7 @@ fi
if [ "$FRONTEND" = "YES" ]; if [ "$FRONTEND" = "YES" ];
then then
YarnInstall YarnInstall
RunWebpack RunGulp
fi fi
if [ "$LINT" = "YES" ]; if [ "$LINT" = "YES" ];
@@ -362,18 +349,14 @@ then
if [[ -z "$RID" || -z "$FRAMEWORK" ]]; if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then then
Package "net5.0" "win-x64" Package "netcoreapp3.1" "win-x64"
Package "net5.0" "win-x86" Package "netcoreapp3.1" "linux-x64"
Package "net5.0" "linux-x64" Package "netcoreapp3.1" "linux-musl-x64"
Package "net5.0" "linux-musl-x64" Package "netcoreapp3.1" "linux-arm64"
Package "net5.0" "linux-arm64" Package "netcoreapp3.1" "linux-musl-arm64"
Package "net5.0" "linux-musl-arm64" Package "netcoreapp3.1" "linux-arm"
Package "net5.0" "linux-arm" Package "netcoreapp3.1" "osx-x64"
Package "net5.0" "osx-x64" Package "net462" "linux-x64"
if [ "$ENABLE_BSD" = "YES" ];
then
Package "net5.0" "freebsd-x64"
fi
else else
Package "$FRAMEWORK" "$RID" Package "$FRAMEWORK" "$RID"
fi fi
+14
View File
@@ -0,0 +1,14 @@
# Changelog
{{#versions}}
## {{{label}}}
{{#sections}}
### {{{label}}}
{{#commits}}
- {{{subject}}} [<a href="https://github.com/{{{author}}}">{{{author}}}</a>]
{{/commits}}
{{/sections}}
{{/versions}}
+15
View File
@@ -0,0 +1,15 @@
**To receive further Pre-Release updates, please change the branch to develop. (Settings -> General (Show Advanced Settings) -> Updates -> Branch)**
{{#versions}}
{{#sections}}
{{{label}}}
{{#commits}}
- {{{subject}}} [{{{author}}}]
{{/commits}}
{{/sections}}
{{/versions}}
**Note**: The OSX version does not automatically launch the browser. You have to go to http://localhost:7878 by yourself in a browser of your choice.
+3 -8
View File
@@ -6,10 +6,8 @@ const dirs = fs
.map((dirent) => dirent.name) .map((dirent) => dirent.name)
.join('|'); .join('|');
const frontendFolder = __dirname;
module.exports = { module.exports = {
parser: '@babel/eslint-parser', parser: 'babel-eslint',
env: { env: {
browser: true, browser: true,
@@ -27,9 +25,6 @@ module.exports = {
parserOptions: { parserOptions: {
ecmaVersion: 6, ecmaVersion: 6,
sourceType: 'module', sourceType: 'module',
babelOptions: {
configFile: `${frontendFolder}/babel.config.js`
},
ecmaFeatures: { ecmaFeatures: {
modules: true, modules: true,
impliedStrict: true impliedStrict: true
@@ -276,7 +271,7 @@ module.exports = {
// ImportSort // ImportSort
'simple-import-sort/imports': 'error', 'simple-import-sort/sort': 'error',
'import/newline-after-import': 'error', 'import/newline-after-import': 'error',
// React // React
@@ -314,7 +309,7 @@ module.exports = {
{ {
files: ['*.js'], files: ['*.js'],
rules: { rules: {
'simple-import-sort/imports': [ 'simple-import-sort/sort': [
'error', 'error',
{ {
groups: [ groups: [
+1 -1
View File
@@ -75,7 +75,7 @@
"function-parentheses-newline-inside": "never-multi-line", "function-parentheses-newline-inside": "never-multi-line",
"function-parentheses-space-inside": "never", "function-parentheses-space-inside": "never",
"function-url-quotes": "always", "function-url-quotes": "always",
"function-url-scheme-disallowed-list": [ "function-url-scheme-blacklist": [
"data" "data"
], ],
"function-whitespace-after": "always", "function-whitespace-after": "always",
-270
View File
@@ -1,270 +0,0 @@
const path = require('path');
const webpack = require('webpack');
const FileManagerPlugin = require('filemanager-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const LiveReloadPlugin = require('webpack-livereload-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = (env) => {
const uiFolder = 'UI';
const frontendFolder = path.join(__dirname, '..');
const srcFolder = path.join(frontendFolder, 'src');
const isProduction = !!env.production;
const isProfiling = isProduction && !!env.profile;
const inlineWebWorkers = 'no-fallback';
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
console.log('Source Folder:', srcFolder);
console.log('Output Folder:', distFolder);
console.log('isProduction:', isProduction);
console.log('isProfiling:', isProfiling);
const config = {
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map',
stats: {
children: false
},
watchOptions: {
ignored: /node_modules/
},
entry: {
index: 'index.js'
},
resolve: {
modules: [
srcFolder,
path.join(srcFolder, 'Shims'),
'node_modules'
],
alias: {
jquery: 'jquery/src/jquery'
},
fallback: {
buffer: false,
http: false,
https: false,
url: false,
util: false,
net: false
}
},
output: {
path: distFolder,
publicPath: '/',
filename: '[name].js',
sourceMapFilename: '[file].map'
},
optimization: {
moduleIds: 'deterministic',
chunkIds: 'named',
splitChunks: {
chunks: 'initial',
name: 'vendors'
}
},
performance: {
hints: false
},
plugins: [
new webpack.DefinePlugin({
__DEV__: !isProduction,
'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development')
}),
new MiniCssExtractPlugin({
filename: 'Content/styles.css'
}),
new HtmlWebpackPlugin({
template: 'frontend/src/index.ejs',
filename: 'index.html',
publicPath: '/'
}),
new FileManagerPlugin({
events: {
onEnd: {
copy: [
// HTML
{
source: 'frontend/src/*.html',
destination: distFolder
},
// Fonts
{
source: 'frontend/src/Content/Fonts/*.*',
destination: path.join(distFolder, 'Content/Fonts')
},
// Icon Images
{
source: 'frontend/src/Content/Images/Icons/*.*',
destination: path.join(distFolder, 'Content/Images/Icons')
},
// Images
{
source: 'frontend/src/Content/Images/*.*',
destination: path.join(distFolder, 'Content/Images')
},
// Robots
{
source: 'frontend/src/Content/robots.txt',
destination: path.join(distFolder, 'Content/robots.txt')
}
]
}
}
}),
new LiveReloadPlugin()
],
resolveLoader: {
modules: [
'node_modules',
'frontend/build/webpack/'
]
},
module: {
rules: [
{
test: /\.worker\.js$/,
use: {
loader: 'worker-loader',
options: {
filename: '[name].js',
inline: inlineWebWorkers
}
}
},
{
test: /\.js?$/,
exclude: /(node_modules|JsLibraries)/,
use: [
{
loader: 'babel-loader',
options: {
configFile: `${frontendFolder}/babel.config.js`,
envName: isProduction ? 'production' : 'development',
presets: [
[
'@babel/preset-env',
{
modules: false,
loose: true,
debug: false,
useBuiltIns: 'entry',
corejs: 3
}
]
]
}
}
]
},
// CSS Modules
{
test: /\.css$/,
exclude: /(node_modules|globals.css)/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: {
localIdentName: '[name]/[local]/[hash:base64:5]'
}
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
config: 'frontend/postcss.config.js'
}
}
}
]
},
// Global styles
{
test: /\.css$/,
include: /(node_modules|globals.css)/,
use: [
'style-loader',
{
loader: 'css-loader'
}
]
},
// Fonts
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
mimetype: 'application/font-woff',
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
}
}
]
},
{
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'file-loader',
options: {
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
}
}
]
}
]
}
};
if (isProfiling) {
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
config.optimization.minimizer = [
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true, // Must be set to true if using source-maps in production
terserOptions: {
mangle: false,
keep_classnames: true,
keep_fnames: true
}
})
];
}
return config;
};
+17
View File
@@ -0,0 +1,17 @@
const gulp = require('gulp');
require('./clean');
require('./copy');
require('./webpack');
gulp.task('build',
gulp.series('clean',
gulp.parallel(
'webpack',
'copyHtml',
'copyFonts',
'copyImages'
)
)
);
+8
View File
@@ -0,0 +1,8 @@
const gulp = require('gulp');
const del = require('del');
const paths = require('./helpers/paths');
gulp.task('clean', () => {
return del([paths.dest.root]);
});
+34
View File
@@ -0,0 +1,34 @@
const path = require('path');
const gulp = require('gulp');
const print = require('gulp-print').default;
const cache = require('gulp-cached');
const livereload = require('gulp-livereload');
const paths = require('./helpers/paths.js');
gulp.task('copyHtml', () => {
return gulp.src(paths.src.html, { base: paths.src.root })
.pipe(cache('copyHtml'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
.pipe(livereload());
});
gulp.task('copyFonts', () => {
return gulp.src(
path.join(paths.src.fonts, '**', '*.*'), { base: paths.src.root }
)
.pipe(cache('copyFonts'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
.pipe(livereload());
});
gulp.task('copyImages', () => {
return gulp.src(
path.join(paths.src.images, '**', '*.*'), { base: paths.src.root }
)
.pipe(cache('copyImages'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
.pipe(livereload());
});
+5
View File
@@ -0,0 +1,5 @@
require('./build.js');
require('./clean.js');
require('./copy.js');
require('./watch.js');
require('./webpack.js');
+6
View File
@@ -0,0 +1,6 @@
const colors = require('ansi-colors');
module.exports = function errorHandler(error) {
console.log(colors.red(`Error (${error.plugin}): ${error.message}`));
this.emit('end');
};
+23
View File
@@ -0,0 +1,23 @@
const root = './frontend/src';
const paths = {
src: {
root,
html: `${root}/*.html`,
scripts: `${root}/**/*.js`,
content: `${root}/Content/`,
fonts: `${root}/Content/Fonts/`,
images: `${root}/Content/Images/`,
exclude: {
libs: `!${root}/JsLibraries/**`
}
},
dest: {
root: './_output/UI/',
content: './_output/UI/Content/',
fonts: './_output/UI/Content/Fonts/',
images: './_output/UI/Content/Images/'
}
};
module.exports = paths;
+18
View File
@@ -0,0 +1,18 @@
const gulp = require('gulp');
const livereload = require('gulp-livereload');
const gulpWatch = require('gulp-watch');
const paths = require('./helpers/paths.js');
require('./copy.js');
require('./webpack.js');
function watch() {
livereload.listen({ start: true });
gulp.task('webpackWatch')();
gulpWatch(paths.src.html, gulp.series('copyHtml'));
gulpWatch(`${paths.src.fonts}**/*.*`, gulp.series('copyFonts'));
gulpWatch(`${paths.src.images}**/*.*`, gulp.series('copyImages'));
}
gulp.task('watch', gulp.series('build', watch));
+266
View File
@@ -0,0 +1,266 @@
const gulp = require('gulp');
const webpackStream = require('webpack-stream');
const livereload = require('gulp-livereload');
const path = require('path');
const webpack = require('webpack');
const errorHandler = require('./helpers/errorHandler');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const uiFolder = 'UI';
const frontendFolder = path.join(__dirname, '..');
const srcFolder = path.join(frontendFolder, 'src');
const isProduction = process.argv.indexOf('--production') > -1;
const isProfiling = isProduction && process.argv.indexOf('--profile') > -1;
const inlineWebWorkers = true;
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
console.log('Source Folder:', srcFolder);
console.log('Output Folder:', distFolder);
console.log('isProduction:', isProduction);
console.log('isProfiling:', isProfiling);
const cssVarsFiles = [
'../src/Styles/Variables/colors',
'../src/Styles/Variables/dimensions',
'../src/Styles/Variables/fonts',
'../src/Styles/Variables/animations',
'../src/Styles/Variables/zIndexes'
].map(require.resolve);
// Override the way HtmlWebpackPlugin injects the scripts
HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, assets, assetTags) {
const head = assetTags.head.map((v) => {
v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${v.attributes.href.replace('\\', '/')}` };
return this.createHtmlTag(v);
});
const body = assetTags.body.map((v) => {
v.attributes = { src: `/${v.attributes.src}` };
return this.createHtmlTag(v);
});
return html
.replace('<!-- webpack bundles head -->', head.join('\r\n '))
.replace('<!-- webpack bundles body -->', body.join('\r\n '));
};
const plugins = [
new OptimizeCssAssetsPlugin({}),
new webpack.DefinePlugin({
__DEV__: !isProduction,
'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development')
}),
new MiniCssExtractPlugin({
filename: path.join('Content', 'styles.css')
}),
new HtmlWebpackPlugin({
template: 'frontend/src/index.html',
filename: 'index.html'
})
];
const config = {
mode: isProduction ? 'production' : 'development',
devtool: '#source-map',
stats: {
children: false
},
watchOptions: {
ignored: /node_modules/
},
entry: {
index: 'index.js'
},
resolve: {
modules: [
srcFolder,
path.join(srcFolder, 'Shims'),
'node_modules'
],
alias: {
jquery: 'jquery/src/jquery'
}
},
output: {
path: distFolder,
filename: '[name].js',
sourceMapFilename: '[file].map'
},
optimization: {
chunkIds: 'named',
splitChunks: {
chunks: 'initial'
}
},
performance: {
hints: false
},
plugins,
resolveLoader: {
modules: [
'node_modules',
'frontend/gulp/webpack/'
]
},
module: {
rules: [
{
test: /\.worker\.js$/,
use: {
loader: 'worker-loader',
options: {
name: '[name].js',
inline: inlineWebWorkers,
fallback: !inlineWebWorkers
}
}
},
{
test: /\.js?$/,
exclude: /(node_modules|JsLibraries)/,
use: [
{
loader: 'babel-loader',
options: {
configFile: `${frontendFolder}/babel.config.js`,
envName: isProduction ? 'production' : 'development',
presets: [
[
'@babel/preset-env',
{
modules: false,
loose: true,
debug: false,
useBuiltIns: 'entry',
corejs: 3
}
]
]
}
}
]
},
// CSS Modules
{
test: /\.css$/,
exclude: /(node_modules|globals.css)/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: {
localIdentName: '[name]/[local]/[hash:base64:5]'
}
}
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
config: {
ctx: {
cssVarsFiles
},
path: 'frontend/postcss.config.js'
}
}
}
]
},
// Global styles
{
test: /\.css$/,
include: /(node_modules|globals.css)/,
use: [
'style-loader',
{
loader: 'css-loader'
}
]
},
// Fonts
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
mimetype: 'application/font-woff',
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
}
}
]
},
{
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'file-loader',
options: {
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
}
}
]
}
]
}
};
if (isProfiling) {
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
config.optimization.minimizer = [
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true, // Must be set to true if using source-maps in production
terserOptions: {
mangle: false,
keep_classnames: true,
keep_fnames: true
}
})
];
}
gulp.task('webpack', () => {
return webpackStream(config)
.pipe(gulp.dest('_output/UI'));
});
gulp.task('webpackWatch', () => {
config.watch = true;
return webpackStream(config, webpack)
.on('error', errorHandler)
.pipe(gulp.dest('_output/UI'))
.on('error', errorHandler)
.pipe(livereload())
.on('error', errorHandler);
});
+20 -29
View File
@@ -1,32 +1,23 @@
const reload = require('require-nocache')(module); const reload = require('require-nocache')(module);
const cssVarsFiles = [ module.exports = (ctx, configPath, options) => {
'./src/Styles/Variables/colors', const config = {
'./src/Styles/Variables/dimensions', plugins: {
'./src/Styles/Variables/fonts', 'postcss-mixins': {
'./src/Styles/Variables/animations', mixinsDir: [
'./src/Styles/Variables/zIndexes' 'frontend/src/Styles/Mixins'
].map(require.resolve); ]
},
'postcss-simple-vars': {
variables: () =>
ctx.options.cssVarsFiles.reduce((acc, vars) => {
return Object.assign(acc, reload(vars));
}, {})
},
'postcss-color-function': {},
'postcss-nested': {}
}
};
const mixinsFiles = [ return config;
'frontend/src/Styles/Mixins/cover.css', };
'frontend/src/Styles/Mixins/linkOverlay.css',
'frontend/src/Styles/Mixins/scroller.css',
'frontend/src/Styles/Mixins/truncate.css'
];
module.exports = {
plugins: [
['postcss-mixins', {
mixinsFiles
}],
['postcss-simple-vars', {
variables: () =>
cssVarsFiles.reduce((acc, vars) => {
return Object.assign(acc, reload(vars));
}, {})
}],
'postcss-color-function',
'postcss-nested'
]
};
@@ -0,0 +1,126 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import BlacklistRowConnector from './BlacklistRowConnector';
class Blacklist extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
columns,
totalRecords,
isClearingBlacklistExecuting,
onClearBlacklistPress,
...otherProps
} = this.props;
return (
<PageContent title={translate('Blacklist')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('Clear')}
iconName={icons.CLEAR}
isSpinning={isClearingBlacklistExecuting}
onPress={onClearBlacklistPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>
{translate('UnableToLoadBlacklist')}
</div>
}
{
isPopulated && !error && !items.length &&
<div>
{translate('NoHistory')}
</div>
}
{
isPopulated && !error && !!items.length &&
<div>
<Table
columns={columns}
{...otherProps}
>
<TableBody>
{
items.map((item) => {
return (
<BlacklistRowConnector
key={item.id}
columns={columns}
{...item}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isFetching}
{...otherProps}
/>
</div>
}
</PageContentBody>
</PageContent>
);
}
}
Blacklist.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
onClearBlacklistPress: PropTypes.func.isRequired
};
export default Blacklist;
@@ -0,0 +1,154 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import * as blacklistActions from 'Store/Actions/blacklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Blacklist from './Blacklist';
function createMapStateToProps() {
return createSelector(
(state) => state.blacklist,
createCommandExecutingSelector(commandNames.CLEAR_BLACKLIST),
(blacklist, isClearingBlacklistExecuting) => {
return {
isClearingBlacklistExecuting,
...blacklist
};
}
);
}
const mapDispatchToProps = {
...blacklistActions,
executeCommand
};
class BlacklistConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchBlacklist,
gotoBlacklistFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchBlacklist();
} else {
gotoBlacklistFirstPage();
}
}
componentDidUpdate(prevProps) {
if (prevProps.isClearingBlacklistExecuting && !this.props.isClearingBlacklistExecuting) {
this.props.gotoBlacklistFirstPage();
}
}
componentWillUnmount() {
this.props.clearBlacklist();
unregisterPagePopulator(this.repopulate);
}
//
// Control
repopulate = () => {
this.props.fetchBlacklist();
}
//
// Listeners
onFirstPagePress = () => {
this.props.gotoBlacklistFirstPage();
}
onPreviousPagePress = () => {
this.props.gotoBlacklistPreviousPage();
}
onNextPagePress = () => {
this.props.gotoBlacklistNextPage();
}
onLastPagePress = () => {
this.props.gotoBlacklistLastPage();
}
onPageSelect = (page) => {
this.props.gotoBlacklistPage({ page });
}
onSortPress = (sortKey) => {
this.props.setBlacklistSort({ sortKey });
}
onTableOptionChange = (payload) => {
this.props.setBlacklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlacklistFirstPage();
}
}
onClearBlacklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
}
onTableOptionChange = (payload) => {
this.props.setBlacklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlacklistFirstPage();
}
}
//
// Render
render() {
return (
<Blacklist
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onClearBlacklistPress={this.onClearBlacklistPress}
{...this.props}
/>
);
}
}
BlacklistConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchBlacklist: PropTypes.func.isRequired,
gotoBlacklistFirstPage: PropTypes.func.isRequired,
gotoBlacklistPreviousPage: PropTypes.func.isRequired,
gotoBlacklistNextPage: PropTypes.func.isRequired,
gotoBlacklistLastPage: PropTypes.func.isRequired,
gotoBlacklistPage: PropTypes.func.isRequired,
setBlacklistSort: PropTypes.func.isRequired,
setBlacklistTableOption: PropTypes.func.isRequired,
clearBlacklist: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(BlacklistConnector)
);
@@ -10,7 +10,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
class BlocklistDetailsModal extends Component { class BlacklistDetailsModal extends Component {
// //
// Render // Render
@@ -78,7 +78,7 @@ class BlocklistDetailsModal extends Component {
} }
} }
BlocklistDetailsModal.propTypes = { BlacklistDetailsModal.propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
@@ -87,4 +87,4 @@ BlocklistDetailsModal.propTypes = {
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
export default BlocklistDetailsModal; export default BlacklistDetailsModal;
@@ -3,7 +3,6 @@ import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats'; import MovieFormats from 'Movie/MovieFormats';
@@ -11,10 +10,10 @@ import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality'; import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink'; import MovieTitleLink from 'Movie/MovieTitleLink';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import BlocklistDetailsModal from './BlocklistDetailsModal'; import BlacklistDetailsModal from './BlacklistDetailsModal';
import styles from './BlocklistRow.css'; import styles from './BlacklistRow.css';
class BlocklistRow extends Component { class BlacklistRow extends Component {
// //
// Lifecycle // Lifecycle
@@ -43,7 +42,6 @@ class BlocklistRow extends Component {
render() { render() {
const { const {
id,
movie, movie,
sourceTitle, sourceTitle,
quality, quality,
@@ -53,9 +51,7 @@ class BlocklistRow extends Component {
protocol, protocol,
indexer, indexer,
message, message,
isSelected,
columns, columns,
onSelectedChange,
onRemovePress onRemovePress
} = this.props; } = this.props;
@@ -65,12 +61,6 @@ class BlocklistRow extends Component {
return ( return (
<TableRow> <TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{ {
columns.map((column) => { columns.map((column) => {
const { const {
@@ -166,7 +156,7 @@ class BlocklistRow extends Component {
/> />
<IconButton <IconButton
title={translate('RemoveFromBlocklist')} title={translate('RemoveFromBlacklist')}
name={icons.REMOVE} name={icons.REMOVE}
kind={kinds.DANGER} kind={kinds.DANGER}
onPress={onRemovePress} onPress={onRemovePress}
@@ -179,7 +169,7 @@ class BlocklistRow extends Component {
}) })
} }
<BlocklistDetailsModal <BlacklistDetailsModal
isOpen={this.state.isDetailsModalOpen} isOpen={this.state.isDetailsModalOpen}
sourceTitle={sourceTitle} sourceTitle={sourceTitle}
protocol={protocol} protocol={protocol}
@@ -193,7 +183,7 @@ class BlocklistRow extends Component {
} }
BlocklistRow.propTypes = { BlacklistRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
movie: PropTypes.object.isRequired, movie: PropTypes.object.isRequired,
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
@@ -204,10 +194,8 @@ BlocklistRow.propTypes = {
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
indexer: PropTypes.string, indexer: PropTypes.string,
message: PropTypes.string, message: PropTypes.string,
isSelected: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired,
onRemovePress: PropTypes.func.isRequired onRemovePress: PropTypes.func.isRequired
}; };
export default BlocklistRow; export default BlacklistRow;
@@ -1,8 +1,8 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { removeBlocklistItem } from 'Store/Actions/blocklistActions'; import { removeFromBlacklist } from 'Store/Actions/blacklistActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector'; import createMovieSelector from 'Store/Selectors/createMovieSelector';
import BlocklistRow from './BlocklistRow'; import BlacklistRow from './BlacklistRow';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
@@ -18,9 +18,9 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) { function createMapDispatchToProps(dispatch, props) {
return { return {
onRemovePress() { onRemovePress() {
dispatch(removeBlocklistItem({ id: props.id })); dispatch(removeFromBlacklist({ id: props.id }));
} }
}; };
} }
export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow); export default connect(createMapStateToProps, createMapDispatchToProps)(BlacklistRow);
@@ -1,235 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import BlocklistRowConnector from './BlocklistRowConnector';
class Blocklist extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isConfirmRemoveModalOpen: false,
items: props.items
};
}
componentDidUpdate(prevProps) {
const {
items
} = this.props;
if (hasDifferentItems(prevProps.items, items)) {
this.setState((state) => {
return {
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
items
};
});
return;
}
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
}
//
// Listeners
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
}
onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true });
}
onRemoveSelectedConfirmed = () => {
this.props.onRemoveSelected(this.getSelectedIds());
this.setState({ isConfirmRemoveModalOpen: false });
}
onConfirmRemoveModalClose = () => {
this.setState({ isConfirmRemoveModalOpen: false });
}
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
columns,
totalRecords,
isRemoving,
isClearingBlocklistExecuting,
onClearBlocklistPress,
...otherProps
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmRemoveModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
return (
<PageContent title={translate('Blocklist')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Remove Selected"
iconName={icons.REMOVE}
isDisabled={!selectedIds.length}
isSpinning={isRemoving}
onPress={this.onRemoveSelectedPress}
/>
<PageToolbarButton
label={translate('Clear')}
iconName={icons.CLEAR}
isSpinning={isClearingBlocklistExecuting}
onPress={onClearBlocklistPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>
{translate('UnableToLoadBlocklist')}
</div>
}
{
isPopulated && !error && !items.length &&
<div>
{translate('NoHistory')}
</div>
}
{
isPopulated && !error && !!items.length &&
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
{...otherProps}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<BlocklistRowConnector
key={item.id}
isSelected={selectedState[item.id] || false}
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isFetching}
{...otherProps}
/>
</div>
}
</PageContentBody>
<ConfirmModal
isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER}
title={translate('RemoveSelected')}
message={translate('AreYouSureYouWantToRemoveTheSelectedItemsFromBlocklist')}
confirmLabel={translate('RemoveSelected')}
onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose}
/>
</PageContent>
);
}
}
Blocklist.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isRemoving: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired,
onClearBlocklistPress: PropTypes.func.isRequired
};
export default Blocklist;
@@ -1,152 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import * as blocklistActions from 'Store/Actions/blocklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Blocklist from './Blocklist';
function createMapStateToProps() {
return createSelector(
(state) => state.blocklist,
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
(blocklist, isClearingBlocklistExecuting) => {
return {
isClearingBlocklistExecuting,
...blocklist
};
}
);
}
const mapDispatchToProps = {
...blocklistActions,
executeCommand
};
class BlocklistConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchBlocklist,
gotoBlocklistFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchBlocklist();
} else {
gotoBlocklistFirstPage();
}
}
componentDidUpdate(prevProps) {
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
this.props.gotoBlocklistFirstPage();
}
}
componentWillUnmount() {
this.props.clearBlocklist();
unregisterPagePopulator(this.repopulate);
}
//
// Control
repopulate = () => {
this.props.fetchBlocklist();
}
//
// Listeners
onFirstPagePress = () => {
this.props.gotoBlocklistFirstPage();
}
onPreviousPagePress = () => {
this.props.gotoBlocklistPreviousPage();
}
onNextPagePress = () => {
this.props.gotoBlocklistNextPage();
}
onLastPagePress = () => {
this.props.gotoBlocklistLastPage();
}
onPageSelect = (page) => {
this.props.gotoBlocklistPage({ page });
}
onRemoveSelected = (ids) => {
this.props.removeBlocklistItems({ ids });
}
onSortPress = (sortKey) => {
this.props.setBlocklistSort({ sortKey });
}
onTableOptionChange = (payload) => {
this.props.setBlocklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlocklistFirstPage();
}
}
onClearBlocklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
}
//
// Render
render() {
return (
<Blocklist
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onClearBlocklistPress={this.onClearBlocklistPress}
{...this.props}
/>
);
}
}
BlocklistConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchBlocklist: PropTypes.func.isRequired,
gotoBlocklistFirstPage: PropTypes.func.isRequired,
gotoBlocklistPreviousPage: PropTypes.func.isRequired,
gotoBlocklistNextPage: PropTypes.func.isRequired,
gotoBlocklistLastPage: PropTypes.func.isRequired,
gotoBlocklistPage: PropTypes.func.isRequired,
removeBlocklistItems: PropTypes.func.isRequired,
setBlocklistSort: PropTypes.func.isRequired,
setBlocklistTableOption: PropTypes.func.isRequired,
clearBlocklist: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector)
);
@@ -173,13 +173,13 @@ function HistoryDetails(props) {
switch (reason) { switch (reason) {
case 'Manual': case 'Manual':
reasonMessage = translate('FileWasDeletedByViaUI'); reasonMessage = 'File was deleted by via UI';
break; break;
case 'MissingFromDisk': case 'MissingFromDisk':
reasonMessage = translate('MissingFromDisk'); reasonMessage = 'Radarr was unable to find the file on disk so it was removed';
break; break;
case 'Upgrade': case 'Upgrade':
reasonMessage = translate('FileWasDeletedByUpgrade'); reasonMessage = 'File was deleted to import an upgrade';
break; break;
default: default:
reasonMessage = ''; reasonMessage = '';
@@ -1,3 +1,4 @@
import _ from 'lodash';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
@@ -7,10 +8,10 @@ function createMapStateToProps() {
return createSelector( return createSelector(
createUISettingsSelector(), createUISettingsSelector(),
(uiSettings) => { (uiSettings) => {
return { return _.pick(uiSettings, [
shortDateFormat: uiSettings.shortDateFormat, 'shortDateFormat',
timeFormat: uiSettings.timeFormat 'timeFormat'
}; ]);
} }
); );
} }
+1 -1
View File
@@ -94,7 +94,7 @@ class History extends Component {
isPopulated && !hasError && !items.length && isPopulated && !hasError && !items.length &&
<div> <div>
{translate('NoHistory')} No history found
</div> </div>
} }
+4 -23
View File
@@ -32,8 +32,6 @@ class Queue extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this._shouldBlockRefresh = false;
this.state = { this.state = {
allSelected: false, allSelected: false,
allUnselected: false, allUnselected: false,
@@ -45,14 +43,6 @@ class Queue extends Component {
}; };
} }
shouldComponentUpdate() {
if (this._shouldBlockRefresh) {
return false;
}
return true;
}
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { const {
items, items,
@@ -95,10 +85,6 @@ class Queue extends Component {
// //
// Listeners // Listeners
onQueueRowModalOpenOrClose = (isOpen) => {
this._shouldBlockRefresh = isOpen;
}
onSelectAllChange = ({ value }) => { onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value)); this.setState(selectAll(this.state.selectedState, value));
} }
@@ -114,19 +100,15 @@ class Queue extends Component {
} }
onRemoveSelectedPress = () => { onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true }, () => { this.setState({ isConfirmRemoveModalOpen: true });
this._shouldBlockRefresh = true;
});
} }
onRemoveSelectedConfirmed = (payload) => { onRemoveSelectedConfirmed = (payload) => {
this._shouldBlockRefresh = false;
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload }); this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
this.setState({ isConfirmRemoveModalOpen: false }); this.setState({ isConfirmRemoveModalOpen: false });
} }
onConfirmRemoveModalClose = () => { onConfirmRemoveModalClose = () => {
this._shouldBlockRefresh = false;
this.setState({ isConfirmRemoveModalOpen: false }); this.setState({ isConfirmRemoveModalOpen: false });
} }
@@ -221,14 +203,14 @@ class Queue extends Component {
{ {
!isRefreshing && hasError && !isRefreshing && hasError &&
<div> <div>
{translate('FailedToLoadQueue')} Failed to load Queue
</div> </div>
} }
{ {
isAllPopulated && !hasError && !items.length && isPopulated && !hasError && !items.length &&
<div> <div>
{translate('QueueIsEmpty')} Queue is empty
</div> </div>
} }
@@ -255,7 +237,6 @@ class Queue extends Component {
columns={columns} columns={columns}
{...item} {...item}
onSelectedChange={this.onSelectedChange} onSelectedChange={this.onSelectedChange}
onQueueRowModalOpenOrClose={this.onQueueRowModalOpenOrClose}
/> />
); );
}) })
@@ -43,7 +43,6 @@ class QueueConnector extends Component {
const { const {
useCurrentPage, useCurrentPage,
fetchQueue, fetchQueue,
fetchQueueStatus,
gotoQueueFirstPage gotoQueueFirstPage
} = this.props; } = this.props;
@@ -54,8 +53,6 @@ class QueueConnector extends Component {
} else { } else {
gotoQueueFirstPage(); gotoQueueFirstPage();
} }
fetchQueueStatus();
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@@ -155,7 +152,6 @@ QueueConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired, useCurrentPage: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchQueue: PropTypes.func.isRequired, fetchQueue: PropTypes.func.isRequired,
fetchQueueStatus: PropTypes.func.isRequired,
gotoQueueFirstPage: PropTypes.func.isRequired, gotoQueueFirstPage: PropTypes.func.isRequired,
gotoQueuePreviousPage: PropTypes.func.isRequired, gotoQueuePreviousPage: PropTypes.func.isRequired,
gotoQueueNextPage: PropTypes.func.isRequired, gotoQueueNextPage: PropTypes.func.isRequired,
+5 -35
View File
@@ -11,14 +11,14 @@ function QueueDetails(props) {
size, size,
sizeleft, sizeleft,
estimatedCompletionTime, estimatedCompletionTime,
status, status: queueStatus,
trackedDownloadState,
trackedDownloadStatus,
errorMessage, errorMessage,
progressBar progressBar
} = props; } = props;
const progress = size ? (100 - sizeleft / size * 100) : 0; const status = queueStatus.toLowerCase();
const progress = (100 - sizeleft / size * 100);
if (status === 'pending') { if (status === 'pending') {
return ( return (
@@ -40,35 +40,7 @@ function QueueDetails(props) {
); );
} }
if (trackedDownloadStatus === 'warning') { // TODO: show an icon when download is complete, but not imported yet?
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.WARNING}
title={translate('UnableToImportCheckLogs')}
/>
);
}
if (trackedDownloadState === 'importPending') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.PURPLE}
title={`${translate('Downloaded')} - ${translate('WaitingToImport')}`}
/>
);
}
if (trackedDownloadState === 'importing') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.PURPLE}
title={`${translate('Downloaded')} - ${translate('Importing')}`}
/>
);
}
} }
if (errorMessage) { if (errorMessage) {
@@ -119,8 +91,6 @@ QueueDetails.propTypes = {
sizeleft: PropTypes.number.isRequired, sizeleft: PropTypes.number.isRequired,
estimatedCompletionTime: PropTypes.string, estimatedCompletionTime: PropTypes.string,
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
errorMessage: PropTypes.string, errorMessage: PropTypes.string,
progressBar: PropTypes.node.isRequired progressBar: PropTypes.node.isRequired
}; };
+3 -17
View File
@@ -42,33 +42,20 @@ class QueueRow extends Component {
this.setState({ isRemoveQueueItemModalOpen: true }); this.setState({ isRemoveQueueItemModalOpen: true });
} }
onRemoveQueueItemModalConfirmed = (blocklist) => { onRemoveQueueItemModalConfirmed = (blacklist) => {
const { this.props.onRemoveQueueItemPress(blacklist);
onRemoveQueueItemPress,
onQueueRowModalOpenOrClose
} = this.props;
onQueueRowModalOpenOrClose(false);
onRemoveQueueItemPress(blocklist);
this.setState({ isRemoveQueueItemModalOpen: false }); this.setState({ isRemoveQueueItemModalOpen: false });
} }
onRemoveQueueItemModalClose = () => { onRemoveQueueItemModalClose = () => {
this.props.onQueueRowModalOpenOrClose(false);
this.setState({ isRemoveQueueItemModalOpen: false }); this.setState({ isRemoveQueueItemModalOpen: false });
} }
onInteractiveImportPress = () => { onInteractiveImportPress = () => {
this.props.onQueueRowModalOpenOrClose(true);
this.setState({ isInteractiveImportModalOpen: true }); this.setState({ isInteractiveImportModalOpen: true });
} }
onInteractiveImportModalClose = () => { onInteractiveImportModalClose = () => {
this.props.onQueueRowModalOpenOrClose(false);
this.setState({ isInteractiveImportModalOpen: false }); this.setState({ isInteractiveImportModalOpen: false });
} }
@@ -372,8 +359,7 @@ QueueRow.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired, onSelectedChange: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired, onGrabPress: PropTypes.func.isRequired,
onRemoveQueueItemPress: PropTypes.func.isRequired, onRemoveQueueItemPress: PropTypes.func.isRequired
onQueueRowModalOpenOrClose: PropTypes.func.isRequired
}; };
QueueRow.defaultProps = { QueueRow.defaultProps = {
@@ -22,7 +22,7 @@ class RemoveQueueItemModal extends Component {
this.state = { this.state = {
remove: true, remove: true,
blocklist: false blacklist: false
}; };
} }
@@ -32,7 +32,7 @@ class RemoveQueueItemModal extends Component {
resetState = function() { resetState = function() {
this.setState({ this.setState({
remove: true, remove: true,
blocklist: false blacklist: false
}); });
} }
@@ -43,8 +43,8 @@ class RemoveQueueItemModal extends Component {
this.setState({ remove: value }); this.setState({ remove: value });
} }
onBlocklistChange = ({ value }) => { onBlacklistChange = ({ value }) => {
this.setState({ blocklist: value }); this.setState({ blacklist: value });
} }
onRemoveConfirmed = () => { onRemoveConfirmed = () => {
@@ -69,7 +69,7 @@ class RemoveQueueItemModal extends Component {
canIgnore canIgnore
} = this.props; } = this.props;
const { remove, blocklist } = this.state; const { remove, blacklist } = this.state;
return ( return (
<Modal <Modal
@@ -81,12 +81,12 @@ class RemoveQueueItemModal extends Component {
onModalClose={this.onModalClose} onModalClose={this.onModalClose}
> >
<ModalHeader> <ModalHeader>
{translate('Remove')} - {sourceTitle} Remove - {sourceTitle}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div> <div>
{translate('RemoveFromQueueText', [sourceTitle])} Are you sure you want to remove '{sourceTitle}' from the queue?
</div> </div>
<FormGroup> <FormGroup>
@@ -103,13 +103,13 @@ class RemoveQueueItemModal extends Component {
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormLabel>{translate('BlocklistRelease')}</FormLabel> <FormLabel>{translate('BlacklistRelease')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="blocklist" name="blacklist"
value={blocklist} value={blacklist}
helpText={translate('BlocklistHelpText')} helpText={translate('BlacklistHelpText')}
onChange={this.onBlocklistChange} onChange={this.onBlacklistChange}
/> />
</FormGroup> </FormGroup>
@@ -23,7 +23,7 @@ class RemoveQueueItemsModal extends Component {
this.state = { this.state = {
remove: true, remove: true,
blocklist: false blacklist: false
}; };
} }
@@ -33,7 +33,7 @@ class RemoveQueueItemsModal extends Component {
resetState = function() { resetState = function() {
this.setState({ this.setState({
remove: true, remove: true,
blocklist: false blacklist: false
}); });
} }
@@ -44,8 +44,8 @@ class RemoveQueueItemsModal extends Component {
this.setState({ remove: value }); this.setState({ remove: value });
} }
onBlocklistChange = ({ value }) => { onBlacklistChange = ({ value }) => {
this.setState({ blocklist: value }); this.setState({ blacklist: value });
} }
onRemoveConfirmed = () => { onRemoveConfirmed = () => {
@@ -70,7 +70,7 @@ class RemoveQueueItemsModal extends Component {
canIgnore canIgnore
} = this.props; } = this.props;
const { remove, blocklist } = this.state; const { remove, blacklist } = this.state;
return ( return (
<Modal <Modal
@@ -87,7 +87,7 @@ class RemoveQueueItemsModal extends Component {
<ModalBody> <ModalBody>
<div className={styles.message}> <div className={styles.message}>
{translate('AreYouSureYouWantToRemoveSelectedItemsFromQueue', [selectedCount, selectedCount > 1 ? 's' : ''])} Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue?
</div> </div>
<FormGroup> <FormGroup>
@@ -105,15 +105,15 @@ class RemoveQueueItemsModal extends Component {
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
Blocklist Release{selectedCount > 1 ? 's' : ''} Blacklist Release{selectedCount > 1 ? 's' : ''}
</FormLabel> </FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="blocklist" name="blacklist"
value={blocklist} value={blacklist}
helpText={translate('BlocklistHelpText')} helpText={translate('BlacklistHelpText')}
onChange={this.onBlocklistChange} onChange={this.onBlacklistChange}
/> />
</FormGroup> </FormGroup>
@@ -81,8 +81,7 @@ class AddNewMovie extends Component {
const { const {
error, error,
items, items,
hasExistingMovies, hasExistingMovies
colorImpairedMode
} = this.props; } = this.props;
const term = this.state.term; const term = this.state.term;
@@ -103,7 +102,7 @@ class AddNewMovie extends Component {
className={styles.searchInput} className={styles.searchInput}
name="movieLookup" name="movieLookup"
value={term} value={term}
placeholder="e.g. The Dark Knight, tmdb:155, imdb:tt0468569" placeholder="eg. The Dark Knight, tmdb:155, imdb:tt0468569"
autoFocus={true} autoFocus={true}
onChange={this.onSearchInputChange} onChange={this.onSearchInputChange}
/> />
@@ -142,7 +141,6 @@ class AddNewMovie extends Component {
return ( return (
<AddNewMovieSearchResultConnector <AddNewMovieSearchResultConnector
key={item.tmdbId} key={item.tmdbId}
colorImpairedMode={colorImpairedMode}
{...item} {...item}
/> />
); );
@@ -161,7 +159,7 @@ class AddNewMovie extends Component {
{translate('YouCanAlsoSearch')} {translate('YouCanAlsoSearch')}
</div> </div>
<div> <div>
<Link to="https://wiki.servarr.com/radarr/faq#why-cant-i-add-a-new-movie-to-radarr"> <Link to="https://github.com/Radarr/Radarr/wiki/FAQ#why-cant-i-add-a-new-movie-when-i-know-the-tmdb-id">
{translate('CantFindMovie')} {translate('CantFindMovie')}
</Link> </Link>
</div> </div>
@@ -215,8 +213,7 @@ AddNewMovie.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
hasExistingMovies: PropTypes.bool.isRequired, hasExistingMovies: PropTypes.bool.isRequired,
onMovieLookupChange: PropTypes.func.isRequired, onMovieLookupChange: PropTypes.func.isRequired,
onClearMovieLookup: PropTypes.func.isRequired, onClearMovieLookup: PropTypes.func.isRequired
colorImpairedMode: PropTypes.bool.isRequired
}; };
export default AddNewMovie; export default AddNewMovie;
@@ -3,10 +3,8 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions'; import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { fetchImportExclusions } from 'Store/Actions/Settings/importExclusions'; import { fetchImportExclusions } from 'Store/Actions/Settings/importExclusions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import parseUrl from 'Utilities/String/parseUrl'; import parseUrl from 'Utilities/String/parseUrl';
import AddNewMovie from './AddNewMovie'; import AddNewMovie from './AddNewMovie';
@@ -15,15 +13,13 @@ function createMapStateToProps() {
(state) => state.addMovie, (state) => state.addMovie,
(state) => state.movies.items.length, (state) => state.movies.items.length,
(state) => state.router.location, (state) => state.router.location,
createUISettingsSelector(), (addMovie, existingMoviesCount, location) => {
(addMovie, existingMoviesCount, location, uiSettings) => {
const { params } = parseUrl(location.search); const { params } = parseUrl(location.search);
return { return {
...addMovie, ...addMovie,
term: params.term, term: params.term,
hasExistingMovies: existingMoviesCount > 0, hasExistingMovies: existingMoviesCount > 0
colorImpairedMode: uiSettings.enableColorImpairedMode
}; };
} }
); );
@@ -33,9 +29,7 @@ const mapDispatchToProps = {
lookupMovie, lookupMovie,
clearAddMovie, clearAddMovie,
fetchRootFolders, fetchRootFolders,
fetchImportExclusions, fetchImportExclusions
fetchQueueDetails,
clearQueueDetails
}; };
class AddNewMovieConnector extends Component { class AddNewMovieConnector extends Component {
@@ -52,7 +46,6 @@ class AddNewMovieConnector extends Component {
componentDidMount() { componentDidMount() {
this.props.fetchRootFolders(); this.props.fetchRootFolders();
this.props.fetchImportExclusions(); this.props.fetchImportExclusions();
this.props.fetchQueueDetails();
} }
componentWillUnmount() { componentWillUnmount() {
@@ -61,7 +54,6 @@ class AddNewMovieConnector extends Component {
} }
this.props.clearAddMovie(); this.props.clearAddMovie();
this.props.clearQueueDetails();
} }
// //
@@ -110,9 +102,7 @@ AddNewMovieConnector.propTypes = {
lookupMovie: PropTypes.func.isRequired, lookupMovie: PropTypes.func.isRequired,
clearAddMovie: PropTypes.func.isRequired, clearAddMovie: PropTypes.func.isRequired,
fetchRootFolders: PropTypes.func.isRequired, fetchRootFolders: PropTypes.func.isRequired,
fetchImportExclusions: PropTypes.func.isRequired, fetchImportExclusions: PropTypes.func.isRequired
fetchQueueDetails: PropTypes.func.isRequired,
clearQueueDetails: PropTypes.func.isRequired
}; };
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewMovieConnector); export default connect(createMapStateToProps, mapDispatchToProps)(AddNewMovieConnector);
@@ -17,15 +17,30 @@ import styles from './AddNewMovieModalContent.css';
class AddNewMovieModalContent extends Component { class AddNewMovieModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
searchForMovie: false
};
}
// //
// Listeners // Listeners
onSearchForMissingMovieChange = ({ value }) => {
this.setState({ searchForMovie: value });
}
onQualityProfileIdChange = ({ value }) => { onQualityProfileIdChange = ({ value }) => {
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) }); this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
} }
onAddMoviePress = () => { onAddMoviePress = () => {
this.props.onAddMoviePress(); this.props.onAddMoviePress(this.state.searchForMovie);
} }
// //
@@ -42,7 +57,6 @@ class AddNewMovieModalContent extends Component {
monitor, monitor,
qualityProfileId, qualityProfileId,
minimumAvailability, minimumAvailability,
searchForMovie,
folder, folder,
tags, tags,
isSmallScreen, isSmallScreen,
@@ -103,7 +117,7 @@ class AddNewMovieModalContent extends Component {
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
{translate('Monitor')} Monitor
</FormLabel> </FormLabel>
<FormInputGroup <FormInputGroup
@@ -154,15 +168,15 @@ class AddNewMovieModalContent extends Component {
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<label className={styles.searchForMissingMovieLabelContainer}> <label className={styles.searchForMissingMovieLabelContainer}>
<span className={styles.searchForMissingMovieLabel}> <span className={styles.searchForMissingMovieLabel}>
{translate('StartSearchForMissingMovie')} Start search for missing movie
</span> </span>
<CheckInput <CheckInput
containerClassName={styles.searchForMissingMovieContainer} containerClassName={styles.searchForMissingMovieContainer}
className={styles.searchForMissingMovieInput} className={styles.searchForMissingMovieInput}
name="searchForMovie" name="searchForMovie"
onChange={onInputChange} value={this.state.searchForMovie}
{...searchForMovie} onChange={this.onSearchForMissingMovieChange}
/> />
</label> </label>
@@ -172,7 +186,7 @@ class AddNewMovieModalContent extends Component {
isSpinning={isAdding} isSpinning={isAdding}
onPress={this.onAddMoviePress} onPress={this.onAddMoviePress}
> >
{translate('AddMovie')} Add {title}
</SpinnerButton> </SpinnerButton>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
@@ -191,7 +205,6 @@ AddNewMovieModalContent.propTypes = {
monitor: PropTypes.object.isRequired, monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object, qualityProfileId: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired, minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired, folder: PropTypes.string.isRequired,
tags: PropTypes.object.isRequired, tags: PropTypes.object.isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
@@ -53,14 +53,13 @@ class AddNewMovieModalContentConnector extends Component {
this.props.setAddMovieDefault({ [name]: value }); this.props.setAddMovieDefault({ [name]: value });
} }
onAddMoviePress = () => { onAddMoviePress = (searchForMovie) => {
const { const {
tmdbId, tmdbId,
rootFolderPath, rootFolderPath,
monitor, monitor,
qualityProfileId, qualityProfileId,
minimumAvailability, minimumAvailability,
searchForMovie,
tags tags
} = this.props; } = this.props;
@@ -70,8 +69,8 @@ class AddNewMovieModalContentConnector extends Component {
monitor: monitor.value, monitor: monitor.value,
qualityProfileId: qualityProfileId.value, qualityProfileId: qualityProfileId.value,
minimumAvailability: minimumAvailability.value, minimumAvailability: minimumAvailability.value,
searchForMovie: searchForMovie.value, tags: tags.value,
tags: tags.value searchForMovie
}); });
} }
@@ -95,7 +94,6 @@ AddNewMovieModalContentConnector.propTypes = {
monitor: PropTypes.object.isRequired, monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object, qualityProfileId: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired, minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired,
tags: PropTypes.object.isRequired, tags: PropTypes.object.isRequired,
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
setAddMovieDefault: PropTypes.func.isRequired, setAddMovieDefault: PropTypes.func.isRequired,
@@ -27,11 +27,9 @@
} }
.poster { .poster {
position: relative; flex: 0 0 170px;
display: block;
margin-right: 20px; margin-right: 20px;
height: 250px; height: 250px;
background-color: $defaultColor;
} }
.content { .content {
@@ -88,28 +86,6 @@
pointer-events: all; pointer-events: all;
} }
.posterContainer {
position: relative;
}
.certification {
margin-left: 2px;
padding: 0 5px;
border: 1px solid;
border-radius: 5px;
font-size: 16px;
}
.runtime {
margin-left: 8px;
font-size: 16px;
}
.statusContainer {
margin-right: 22px;
font-weight: bold;
}
@media only screen and (max-width: $breakpointMedium) { @media only screen and (max-width: $breakpointMedium) {
.titleRow { .titleRow {
justify-content: space-between; justify-content: space-between;
@@ -7,10 +7,7 @@ import Link from 'Components/Link/Link';
import Tooltip from 'Components/Tooltip/Tooltip'; import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props'; import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks'; import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import MovieStatusLabel from 'Movie/Details/MovieStatusLabel';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MoviePoster from 'Movie/MoviePoster'; import MoviePoster from 'Movie/MoviePoster';
import formatRuntime from 'Utilities/Date/formatRuntime';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import AddNewMovieModal from './AddNewMovieModal'; import AddNewMovieModal from './AddNewMovieModal';
import styles from './AddNewMovieSearchResult.css'; import styles from './AddNewMovieSearchResult.css';
@@ -68,17 +65,7 @@ class AddNewMovieSearchResult extends Component {
images, images,
isExistingMovie, isExistingMovie,
isExclusionMovie, isExclusionMovie,
isSmallScreen, isSmallScreen
colorImpairedMode,
id,
monitored,
hasFile,
isAvailable,
queueStatus,
queueState,
runtime,
movieRuntimeFormat,
certification
} = this.props; } = this.props;
const { const {
@@ -98,30 +85,12 @@ class AddNewMovieSearchResult extends Component {
{ {
isSmallScreen ? isSmallScreen ?
null : null :
<div> <MoviePoster
<div className={styles.posterContainer}> className={styles.poster}
<MoviePoster images={images}
className={styles.poster} size={250}
images={images} overflow={true}
size={250} />
overflow={true}
/>
</div>
{
isExistingMovie &&
<MovieIndexProgressBar
monitored={monitored}
hasFile={hasFile}
status={status}
posterWidth={167}
detailedProgressBar={true}
queueStatus={queueStatus}
queueState={queueState}
isAvailable={isAvailable}
/>
}
</div>
} }
<div className={styles.content}> <div className={styles.content}>
@@ -164,22 +133,6 @@ class AddNewMovieSearchResult extends Component {
</div> </div>
</div> </div>
<div>
{
!!certification &&
<span className={styles.certification}>
{certification}
</span>
}
{
!!runtime &&
<span className={styles.runtime}>
{formatRuntime(runtime, movieRuntimeFormat)}
</span>
}
</div>
<div> <div>
<Label size={sizes.LARGE}> <Label size={sizes.LARGE}>
<HeartRating <HeartRating
@@ -217,21 +170,18 @@ class AddNewMovieSearchResult extends Component {
imdbId={imdbId} imdbId={imdbId}
/> />
} }
canFlip={true}
kind={kinds.INVERSE} kind={kinds.INVERSE}
position={tooltipPositions.BOTTOM} position={tooltipPositions.BOTTOM}
/> />
{ {
isExistingMovie && isSmallScreen && status === 'ended' &&
<MovieStatusLabel <Label
hasMovieFiles={hasFile} kind={kinds.DANGER}
monitored={monitored} size={sizes.LARGE}
isAvailable={isAvailable} >
id={id} Ended
useLabel={true} </Label>
colorImpairedMode={colorImpairedMode}
/>
} }
</div> </div>
@@ -271,18 +221,7 @@ AddNewMovieSearchResult.propTypes = {
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
isExistingMovie: PropTypes.bool.isRequired, isExistingMovie: PropTypes.bool.isRequired,
isExclusionMovie: PropTypes.bool.isRequired, isExclusionMovie: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired
id: PropTypes.number,
queueItems: PropTypes.arrayOf(PropTypes.object),
monitored: PropTypes.bool.isRequired,
hasFile: PropTypes.bool.isRequired,
isAvailable: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool,
queueStatus: PropTypes.string,
queueState: PropTypes.string,
runtime: PropTypes.number.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired,
certification: PropTypes.string
}; };
export default AddNewMovieSearchResult; export default AddNewMovieSearchResult;
@@ -10,17 +10,11 @@ function createMapStateToProps() {
createExistingMovieSelector(), createExistingMovieSelector(),
createExclusionMovieSelector(), createExclusionMovieSelector(),
createDimensionsSelector(), createDimensionsSelector(),
(state) => state.queue.details.items, (isExistingMovie, isExclusionMovie, dimensions) => {
(state, { internalId }) => internalId,
(isExistingMovie, isExclusionMovie, dimensions, queueItems, internalId) => {
const firstQueueItem = queueItems.find((q) => q.movieId === internalId && internalId > 0);
return { return {
isExistingMovie, isExistingMovie,
isExclusionMovie, isExclusionMovie,
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen
queueStatus: firstQueueItem ? firstQueueItem.status : null,
queueState: firstQueueItem ? firstQueueItem.trackedDownloadState : null
}; };
} }
); );
@@ -116,7 +116,7 @@ class ImportMovie extends Component {
rootFoldersPopulated && rootFoldersPopulated &&
!unmappedFolders.length ? !unmappedFolders.length ?
<div> <div>
{translate('AllMoviesInPathHaveBeenImported', [path])} All movies in {path} have been imported
</div> : </div> :
null null
} }
@@ -10,7 +10,6 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContentFooter from 'Components/Page/PageContentFooter'; import PageContentFooter from 'Components/Page/PageContentFooter';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props'; import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ImportMovieFooter.css'; import styles from './ImportMovieFooter.css';
const MIXED = 'mixed'; const MIXED = 'mixed';
@@ -113,7 +112,7 @@ class ImportMovieFooter extends Component {
<PageContentFooter> <PageContentFooter>
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<div className={styles.label}> <div className={styles.label}>
{translate('Monitor')} Monitor
</div> </div>
<FormInputGroup <FormInputGroup
@@ -128,7 +127,7 @@ class ImportMovieFooter extends Component {
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<div className={styles.label}> <div className={styles.label}>
{translate('MinimumAvailability')} Minimum Availability
</div> </div>
<FormInputGroup <FormInputGroup
@@ -143,7 +142,7 @@ class ImportMovieFooter extends Component {
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<div className={styles.label}> <div className={styles.label}>
{translate('QualityProfile')} Quality Profile
</div> </div>
<FormInputGroup <FormInputGroup
@@ -169,7 +168,7 @@ class ImportMovieFooter extends Component {
isDisabled={!selectedCount || isLookingUpMovie} isDisabled={!selectedCount || isLookingUpMovie}
onPress={onImportPress} onPress={onImportPress}
> >
{translate('Import')} {selectedCount} {selectedCount > 1 ? translate('Movies') : translate('Movie')} Import {selectedCount} {selectedCount > 1 ? 'Movies' : 'Movie'}
</SpinnerButton> </SpinnerButton>
{ {
@@ -179,7 +178,7 @@ class ImportMovieFooter extends Component {
kind={kinds.WARNING} kind={kinds.WARNING}
onPress={onCancelLookupPress} onPress={onCancelLookupPress}
> >
{translate('CancelProcessing')} Cancel Processing
</Button> : </Button> :
null null
} }
@@ -191,7 +190,7 @@ class ImportMovieFooter extends Component {
kind={kinds.SUCCESS} kind={kinds.SUCCESS}
onPress={onLookupPress} onPress={onLookupPress}
> >
{translate('StartProcessing')} Start Processing
</Button> : </Button> :
null null
} }
@@ -207,7 +206,7 @@ class ImportMovieFooter extends Component {
{ {
isLookingUpMovie ? isLookingUpMovie ?
translate('ProcessingFolders') : 'Processing Folders' :
null null
} }
@@ -221,7 +220,7 @@ class ImportMovieFooter extends Component {
kind={kinds.WARNING} kind={kinds.WARNING}
/> />
} }
title={translate('ImportErrors')} title="Import Errors"
body={ body={
<ul> <ul>
{ {
@@ -3,7 +3,6 @@ import React from 'react';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader'; import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell'; import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell'; import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import translate from 'Utilities/String/translate';
import styles from './ImportMovieHeader.css'; import styles from './ImportMovieHeader.css';
function ImportMovieHeader(props) { function ImportMovieHeader(props) {
@@ -25,35 +24,35 @@ function ImportMovieHeader(props) {
className={styles.folder} className={styles.folder}
name="folder" name="folder"
> >
{translate('Folder')} Folder
</VirtualTableHeaderCell>
<VirtualTableHeaderCell
className={styles.movie}
name="movie"
>
{translate('Movie')}
</VirtualTableHeaderCell> </VirtualTableHeaderCell>
<VirtualTableHeaderCell <VirtualTableHeaderCell
className={styles.monitor} className={styles.monitor}
name="monitor" name="monitor"
> >
{translate('Monitor')} Monitor
</VirtualTableHeaderCell> </VirtualTableHeaderCell>
<VirtualTableHeaderCell <VirtualTableHeaderCell
className={styles.minimumAvailability} className={styles.minimumAvailability}
name="minimumAvailability" name="minimumAvailability"
> >
{translate('MinAvailability')} Min Availability
</VirtualTableHeaderCell> </VirtualTableHeaderCell>
<VirtualTableHeaderCell <VirtualTableHeaderCell
className={styles.qualityProfile} className={styles.qualityProfile}
name="qualityProfileId" name="qualityProfileId"
> >
{translate('QualityProfile')} Quality Profile
</VirtualTableHeaderCell>
<VirtualTableHeaderCell
className={styles.movie}
name="movie"
>
Movie
</VirtualTableHeaderCell> </VirtualTableHeaderCell>
</VirtualTableHeader> </VirtualTableHeader>
); );
@@ -34,13 +34,6 @@ function ImportMovieRow(props) {
{id} {id}
</VirtualTableRowCell> </VirtualTableRowCell>
<VirtualTableRowCell className={styles.movie}>
<ImportMovieSelectMovieConnector
id={id}
isExistingMovie={isExistingMovie}
/>
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.monitor}> <VirtualTableRowCell className={styles.monitor}>
<FormInputGroup <FormInputGroup
type={inputTypes.MOVIE_MONITORED_SELECT} type={inputTypes.MOVIE_MONITORED_SELECT}
@@ -67,6 +60,13 @@ function ImportMovieRow(props) {
onChange={onInputChange} onChange={onInputChange}
/> />
</VirtualTableRowCell> </VirtualTableRowCell>
<VirtualTableRowCell className={styles.movie}>
<ImportMovieSelectMovieConnector
id={id}
isExistingMovie={isExistingMovie}
/>
</VirtualTableRowCell>
</> </>
); );
} }
@@ -9,7 +9,6 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Portal from 'Components/Portal'; import Portal from 'Components/Portal';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import getUniqueElememtId from 'Utilities/getUniqueElementId'; import getUniqueElememtId from 'Utilities/getUniqueElementId';
import translate from 'Utilities/String/translate';
import ImportMovieSearchResultConnector from './ImportMovieSearchResultConnector'; import ImportMovieSearchResultConnector from './ImportMovieSearchResultConnector';
import ImportMovieTitle from './ImportMovieTitle'; import ImportMovieTitle from './ImportMovieTitle';
import styles from './ImportMovieSelectMovie.css'; import styles from './ImportMovieSelectMovie.css';
@@ -175,7 +174,7 @@ class ImportMovieSelectMovie extends Component {
kind={kinds.WARNING} kind={kinds.WARNING}
/> />
{translate('NoMatchFound')} No match found!
</div> : </div> :
null null
} }
@@ -190,7 +189,7 @@ class ImportMovieSelectMovie extends Component {
kind={kinds.WARNING} kind={kinds.WARNING}
/> />
{translate('SearchFailedPleaseTryAgainLater')} Search failed, please try again later.
</div> : </div> :
null null
} }
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Label from 'Components/Label'; import Label from 'Components/Label';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ImportMovieTitle.css'; import styles from './ImportMovieTitle.css';
function ImportMovieTitle(props) { function ImportMovieTitle(props) {
@@ -34,7 +33,7 @@ function ImportMovieTitle(props) {
<Label <Label
kind={kinds.WARNING} kind={kinds.WARNING}
> >
{translate('Existing')} Existing
</Label> </Label>
} }
</div> </div>
@@ -30,9 +30,3 @@
.importButtonIcon { .importButtonIcon {
margin-right: 8px; margin-right: 8px;
} }
.addErrorAlert {
composes: alert from '~Components/Alert.css';
margin: 20px 0;
}
@@ -1,6 +1,5 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet'; import FieldSet from 'Components/FieldSet';
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal'; import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
@@ -73,29 +72,23 @@ class ImportMovieSelectFolder extends Component {
isWindows, isWindows,
isFetching, isFetching,
isPopulated, isPopulated,
isSaving,
error, error,
saveError,
items items
} = this.props; } = this.props;
const hasRootFolders = items.length > 0;
return ( return (
<PageContent title={translate('ImportMovies')}> <PageContent title={translate('ImportMovies')}>
<PageContentBody> <PageContentBody>
{ {
isFetching && !isPopulated ? isFetching && !isPopulated &&
<LoadingIndicator /> : <LoadingIndicator />
null
} }
{ {
!isFetching && error ? !isFetching && !!error &&
<div> <div>
{translate('UnableToLoadRootFolders')} {translate('UnableToLoadRootFolders')}
</div> : </div>
null
} }
{ {
@@ -108,14 +101,17 @@ class ImportMovieSelectFolder extends Component {
<div className={styles.tips}> <div className={styles.tips}>
{translate('ImportTipsMessage')} {translate('ImportTipsMessage')}
<ul> <ul>
<li className={styles.tip} dangerouslySetInnerHTML={{ __html: translate('ImportIncludeQuality', ['<code>movie.2008.bluray.mkv</code>']) }} /> <li className={styles.tip}>
<li className={styles.tip} dangerouslySetInnerHTML={{ __html: translate('ImportRootPath', [`<code>${isWindows ? 'C:\\movies' : '/movies'}</code>`, `<code>${isWindows ? 'C:\\movies\\the matrix' : '/movies/the matrix'}</code>`]) }} /> Make sure that your files include the quality in their filenames. eg. <span className={styles.code}>movie.2008.bluray.mkv</span>
<li className={styles.tip}>{translate('ImportNotForDownloads')}</li> </li>
<li className={styles.tip}>
Point Radarr to the folder containing all of your movies, not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\movies' : '/movies'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\movies\\the matrix' : '/movies/the matrix'}"</span>
</li>
</ul> </ul>
</div> </div>
{ {
hasRootFolders ? items.length > 0 ?
<div className={styles.recentFolders}> <div className={styles.recentFolders}>
<FieldSet legend={translate('RecentFolders')}> <FieldSet legend={translate('RecentFolders')}>
<Table <Table
@@ -138,51 +134,35 @@ class ImportMovieSelectFolder extends Component {
</TableBody> </TableBody>
</Table> </Table>
</FieldSet> </FieldSet>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
{translate('ChooseAnotherFolder')}
</Button>
</div> : </div> :
null
<div className={styles.startImport}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
Start Import
</Button>
</div>
} }
{
!isSaving && saveError ?
<Alert
className={styles.addErrorAlert}
kind={kinds.DANGER}
>
{translate('UnableToAddRootFolder')}
<ul>
{
saveError.responseJSON.map((e, index) => {
return (
<li key={index}>
{e.errorMessage}
</li>
);
})
}
</ul>
</Alert> :
null
}
<div className={hasRootFolders ? undefined : styles.startImport}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
{
hasRootFolders ?
translate('ChooseAnotherFolder') :
translate('StartImport')
}
</Button>
</div>
<FileBrowserModal <FileBrowserModal
isOpen={this.state.isAddNewRootFolderModalOpen} isOpen={this.state.isAddNewRootFolderModalOpen}
name="rootFolderPath" name="rootFolderPath"
@@ -202,9 +182,7 @@ ImportMovieSelectFolder.propTypes = {
isWindows: PropTypes.bool.isRequired, isWindows: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
saveError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
onNewRootFolderSelect: PropTypes.func.isRequired, onNewRootFolderSelect: PropTypes.func.isRequired,
onDeleteRootFolderPress: PropTypes.func.isRequired onDeleteRootFolderPress: PropTypes.func.isRequired
+3 -3
View File
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { Redirect, Route } from 'react-router-dom'; import { Redirect, Route } from 'react-router-dom';
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector'; import BlacklistConnector from 'Activity/Blacklist/BlacklistConnector';
import HistoryConnector from 'Activity/History/HistoryConnector'; import HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector'; import QueueConnector from 'Activity/Queue/QueueConnector';
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector'; import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
@@ -111,8 +111,8 @@ function AppRoutes(props) {
/> />
<Route <Route
path="/activity/blocklist" path="/activity/blacklist"
component={BlocklistConnector} component={BlacklistConnector}
/> />
{/* {/*
+9 -48
View File
@@ -11,47 +11,9 @@ import UpdateChanges from 'System/Updates/UpdateChanges';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './AppUpdatedModalContent.css'; import styles from './AppUpdatedModalContent.css';
function mergeUpdates(items, version, prevVersion) {
let installedIndex = items.findIndex((u) => u.version === version);
let installedPreviouslyIndex = items.findIndex((u) => u.version === prevVersion);
if (installedIndex === -1) {
installedIndex = 0;
}
if (installedPreviouslyIndex === -1) {
installedPreviouslyIndex = items.length;
} else if (installedPreviouslyIndex === installedIndex && items.length) {
installedPreviouslyIndex += 1;
}
const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
if (!appliedUpdates.length) {
return null;
}
const appliedChanges = { new: [], fixed: [] };
appliedUpdates.forEach((u) => {
if (u.changes) {
appliedChanges.new.push(... u.changes.new);
appliedChanges.fixed.push(... u.changes.fixed);
}
});
const mergedUpdate = Object.assign({}, appliedUpdates[0], { changes: appliedChanges });
if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
mergedUpdate.changes = null;
}
return mergedUpdate;
}
function AppUpdatedModalContent(props) { function AppUpdatedModalContent(props) {
const { const {
version, version,
prevVersion,
isPopulated, isPopulated,
error, error,
items, items,
@@ -59,32 +21,32 @@ function AppUpdatedModalContent(props) {
onModalClose onModalClose
} = props; } = props;
const update = mergeUpdates(items, version, prevVersion); const update = items[0];
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{translate('RadarrUpdated')} Radarr Updated
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div dangerouslySetInnerHTML={{ __html: translate('VersionUpdateText', [`<span className=${styles.version}>${version}</span>`]) }} /> <div>
Version <span className={styles.version}>{version}</span> of Radarr has been installed, in order to get the latest changes you'll need to reload Radarr.
</div>
{ {
isPopulated && !error && !!update && isPopulated && !error && !!update &&
<div> <div>
{ {
!update.changes && !update.changes &&
<div className={styles.maintenance}> <div className={styles.maintenance}>Maintenance release</div>
{translate('MaintenanceRelease')}
</div>
} }
{ {
!!update.changes && !!update.changes &&
<div> <div>
<div className={styles.changes}> <div className={styles.changes}>
{translate('WhatsNew')} What's new?
</div> </div>
<UpdateChanges <UpdateChanges
@@ -111,14 +73,14 @@ function AppUpdatedModalContent(props) {
<Button <Button
onPress={onSeeChangesPress} onPress={onSeeChangesPress}
> >
{translate('RecentChanges')} Recent Changes
</Button> </Button>
<Button <Button
kind={kinds.PRIMARY} kind={kinds.PRIMARY}
onPress={onModalClose} onPress={onModalClose}
> >
{translate('Reload')} Reload
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
@@ -127,7 +89,6 @@ function AppUpdatedModalContent(props) {
AppUpdatedModalContent.propTypes = { AppUpdatedModalContent.propTypes = {
version: PropTypes.string.isRequired, version: PropTypes.string.isRequired,
prevVersion: PropTypes.string,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
@@ -8,9 +8,8 @@ import AppUpdatedModalContent from './AppUpdatedModalContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.app.version, (state) => state.app.version,
(state) => state.app.prevVersion,
(state) => state.system.updates, (state) => state.system.updates,
(version, prevVersion, updates) => { (version, updates) => {
const { const {
isPopulated, isPopulated,
error, error,
@@ -19,7 +18,6 @@ function createMapStateToProps() {
return { return {
version, version,
prevVersion,
isPopulated, isPopulated,
error, error,
items items
+2 -33
View File
@@ -6,38 +6,9 @@ import styles from './Agenda.css';
function Agenda(props) { function Agenda(props) {
const { const {
items, items
start,
end
} = props; } = props;
const startDateParsed = Date.parse(start);
const endDateParsed = Date.parse(end);
items.forEach((item) => {
const cinemaDateParsed = Date.parse(item.inCinemas);
const digitalDateParsed = Date.parse(item.digitalRelease);
const physicalDateParsed = Date.parse(item.physicalRelease);
const dates = [];
if (cinemaDateParsed > 0 && cinemaDateParsed >= startDateParsed && cinemaDateParsed <= endDateParsed) {
dates.push(cinemaDateParsed);
}
if (digitalDateParsed > 0 && digitalDateParsed >= startDateParsed && digitalDateParsed <= endDateParsed) {
dates.push(digitalDateParsed);
}
if (physicalDateParsed > 0 && physicalDateParsed >= startDateParsed && physicalDateParsed <= endDateParsed) {
dates.push(physicalDateParsed);
}
item.sortDate = Math.min(...dates);
item.cinemaDateParsed = cinemaDateParsed;
item.digitalDateParsed = digitalDateParsed;
item.physicalDateParsed = physicalDateParsed;
});
items.sort((a, b) => ((a.sortDate > b.sortDate) ? 1 : -1));
return ( return (
<div className={styles.agenda}> <div className={styles.agenda}>
{ {
@@ -61,9 +32,7 @@ function Agenda(props) {
} }
Agenda.propTypes = { Agenda.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired
start: PropTypes.string.isRequired,
end: PropTypes.string.isRequired
}; };
export default Agenda; export default Agenda;
+9 -12
View File
@@ -29,7 +29,7 @@
} }
.time { .time {
flex: 0 0 125px; flex: 0 0 120px;
margin-right: 10px; margin-right: 10px;
border: none !important; border: none !important;
} }
@@ -54,24 +54,20 @@
composes: downloaded from '~Calendar/Events/CalendarEvent.css'; composes: downloaded from '~Calendar/Events/CalendarEvent.css';
} }
.queue { .downloading {
composes: queue from '~Calendar/Events/CalendarEvent.css'; composes: downloading from '~Calendar/Events/CalendarEvent.css';
} }
.unmonitored { .unmonitored {
composes: unmonitored from '~Calendar/Events/CalendarEvent.css'; composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
} }
.missingUnmonitored { .missing {
composes: missingUnmonitored from '~Calendar/Events/CalendarEvent.css'; composes: missing from '~Calendar/Events/CalendarEvent.css';
} }
.missingMonitored { .unreleased {
composes: missingMonitored from '~Calendar/Events/CalendarEvent.css'; composes: unreleased from '~Calendar/Events/CalendarEvent.css';
}
.continuing {
composes: continuing from '~Calendar/Events/CalendarEvent.css';
} }
@media only screen and (max-width: $breakpointSmall) { @media only screen and (max-width: $breakpointSmall) {
@@ -96,5 +92,6 @@
} }
.dateIcon { .dateIcon {
width: 25px; display: inline;
margin-right: 10px;
} }
+20 -26
View File
@@ -3,10 +3,10 @@ import moment from 'moment';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails'; import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
import getStatusStyle from 'Calendar/getStatusStyle';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './AgendaEvent.css'; import styles from './AgendaEvent.css';
@@ -55,26 +55,23 @@ class AgendaEvent extends Component {
showCutoffUnmetIcon, showCutoffUnmetIcon,
longDateFormat, longDateFormat,
colorImpairedMode, colorImpairedMode,
cinemaDateParsed, startDate,
digitalDateParsed, endDate
physicalDateParsed,
sortDate
} = this.props; } = this.props;
let startTime = null; const agendaStart = Date.parse(startDate);
let releaseIcon = null; const agendaEnd = Date.parse(endDate);
const cinemaDate = Date.parse(inCinemas);
const digitalDate = Date.parse(digitalRelease);
let startTime = physicalRelease;
let releaseIcon = icons.DISC;
if (physicalDateParsed === sortDate) { if (digitalDate >= agendaStart && digitalDate <= agendaEnd) {
startTime = physicalRelease;
releaseIcon = icons.DISC;
}
if (digitalDateParsed === sortDate) {
startTime = digitalRelease; startTime = digitalRelease;
releaseIcon = icons.MOVIE_FILE; releaseIcon = icons.MOVIE_FILE;
} }
if (cinemaDateParsed === sortDate) { if (cinemaDate >= agendaStart && cinemaDate <= agendaEnd) {
startTime = inCinemas; startTime = inCinemas;
releaseIcon = icons.IN_CINEMAS; releaseIcon = icons.IN_CINEMAS;
} }
@@ -82,7 +79,7 @@ class AgendaEvent extends Component {
startTime = moment(startTime); startTime = moment(startTime);
const downloading = !!(queueItem || grabbed); const downloading = !!(queueItem || grabbed);
const isMonitored = monitored; const isMonitored = monitored;
const statusStyle = getStatusStyle(null, isMonitored, hasFile, isAvailable, 'style', downloading); const statusStyle = getStatusStyle(hasFile, downloading, isAvailable, isMonitored);
const joinedGenres = genres.slice(0, 2).join(', '); const joinedGenres = genres.slice(0, 2).join(', ');
const link = `/movie/${titleSlug}`; const link = `/movie/${titleSlug}`;
@@ -95,14 +92,13 @@ class AgendaEvent extends Component {
)} )}
to={link} to={link}
> >
<div className={styles.dateIcon}>
<Icon
name={releaseIcon}
kind={kinds.DEFAULT}
/>
</div>
<div className={styles.date}> <div className={styles.date}>
<div className={styles.dateIcon}>
<Icon
name={releaseIcon}
kind={kinds.DEFAULT}
/>
</div>
{(showDate) ? startTime.format(longDateFormat) : null} {(showDate) ? startTime.format(longDateFormat) : null}
</div> </div>
@@ -180,10 +176,8 @@ AgendaEvent.propTypes = {
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired, longDateFormat: PropTypes.string.isRequired,
colorImpairedMode: PropTypes.bool.isRequired, colorImpairedMode: PropTypes.bool.isRequired,
cinemaDateParsed: PropTypes.number, startDate: PropTypes.date,
digitalDateParsed: PropTypes.number, endDate: PropTypes.date
physicalDateParsed: PropTypes.number,
sortDate: PropTypes.number
}; };
AgendaEvent.defaultProps = { AgendaEvent.defaultProps = {
@@ -13,7 +13,9 @@ function createMapStateToProps() {
createMovieFileSelector(), createMovieFileSelector(),
createQueueItemSelector(), createQueueItemSelector(),
createUISettingsSelector(), createUISettingsSelector(),
(calendarOptions, movie, movieFile, queueItem, uiSettings) => { (state) => state.calendar.start,
(state) => state.calendar.end,
(calendarOptions, movie, movieFile, queueItem, uiSettings, startDate, endDate) => {
return { return {
movie, movie,
movieFile, movieFile,
@@ -21,7 +23,9 @@ function createMapStateToProps() {
...calendarOptions, ...calendarOptions,
timeFormat: uiSettings.timeFormat, timeFormat: uiSettings.timeFormat,
longDateFormat: uiSettings.longDateFormat, longDateFormat: uiSettings.longDateFormat,
colorImpairedMode: uiSettings.enableColorImpairedMode colorImpairedMode: uiSettings.enableColorImpairedMode,
startDate,
endDate
}; };
} }
); );
@@ -24,7 +24,7 @@ function createMissingMovieIdsSelector() {
const inCinemas = movie.inCinemas; const inCinemas = movie.inCinemas;
if ( if (
!movie.hasFile && !movie.movieFileId &&
moment(inCinemas).isAfter(start) && moment(inCinemas).isAfter(start) &&
moment(inCinemas).isBefore(end) && moment(inCinemas).isBefore(end) &&
isBefore(movie.inCinemas) && isBefore(movie.inCinemas) &&
+18 -9
View File
@@ -60,30 +60,39 @@
} }
} }
.queue { .downloading {
border-left-color: $purple !important; border-left-color: $purple !important;
} }
.unmonitored { .unmonitored {
border-left-color: $gray !important; border-left-color: $gray !important;
}
.missingUnmonitored {
border-left-color: $warningColor !important;
&:global(.colorImpaired) { &:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px); background: repeating-linear-gradient(45deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
} }
} }
.missingMonitored { .onAir {
border-left-color: $dangerColor !important; border-left-color: $warningColor !important;
&:global(.colorImpaired) { &:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px); background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
} }
} }
.continuing { .missing {
border-left-color: $primaryColor !important; border-left-color: $dangerColor !important;
&:global(.colorImpaired) {
border-left-color: color($dangerColor saturation(+15%)) !important;
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
.unreleased {
border-left-color: $primaryColor !important;
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
} }
@@ -2,10 +2,10 @@ import classNames from 'classnames';
import moment from 'moment'; import moment from 'moment';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import getStatusStyle from 'Calendar/getStatusStyle';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import CalendarEventQueueDetails from './CalendarEventQueueDetails'; import CalendarEventQueueDetails from './CalendarEventQueueDetails';
import styles from './CalendarEvent.css'; import styles from './CalendarEvent.css';
@@ -38,7 +38,7 @@ class CalendarEvent extends Component {
const isDownloading = !!(queueItem || grabbed); const isDownloading = !!(queueItem || grabbed);
const isMonitored = monitored; const isMonitored = monitored;
const statusStyle = getStatusStyle(null, isMonitored, hasFile, isAvailable, 'style', isDownloading); const statusStyle = getStatusStyle(hasFile, isDownloading, isAvailable, isMonitored);
const joinedGenres = genres.slice(0, 2).join(', '); const joinedGenres = genres.slice(0, 2).join(', ');
const link = `/movie/${titleSlug}`; const link = `/movie/${titleSlug}`;
const eventType = []; const eventType = [];
@@ -12,12 +12,10 @@ function CalendarEventQueueDetails(props) {
sizeleft, sizeleft,
estimatedCompletionTime, estimatedCompletionTime,
status, status,
trackedDownloadState,
trackedDownloadStatus,
errorMessage errorMessage
} = props; } = props;
const progress = size ? (100 - sizeleft / size * 100) : 0; const progress = (100 - sizeleft / size * 100);
return ( return (
<QueueDetails <QueueDetails
@@ -26,8 +24,6 @@ function CalendarEventQueueDetails(props) {
sizeleft={sizeleft} sizeleft={sizeleft}
estimatedCompletionTime={estimatedCompletionTime} estimatedCompletionTime={estimatedCompletionTime}
status={status} status={status}
trackedDownloadState={trackedDownloadState}
trackedDownloadStatus={trackedDownloadStatus}
errorMessage={errorMessage} errorMessage={errorMessage}
progressBar={ progressBar={
<div title={translate('MovieIsDownloadingInterp', [progress.toFixed(1), title])}> <div title={translate('MovieIsDownloadingInterp', [progress.toFixed(1), title])}>
@@ -49,8 +45,6 @@ CalendarEventQueueDetails.propTypes = {
sizeleft: PropTypes.number.isRequired, sizeleft: PropTypes.number.isRequired,
estimatedCompletionTime: PropTypes.string, estimatedCompletionTime: PropTypes.string,
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
errorMessage: PropTypes.string errorMessage: PropTypes.string
}; };
@@ -24,7 +24,7 @@ function getTitle(time, start, end, view, longDateFormat) {
} else if (view === 'month') { } else if (view === 'month') {
return timeMoment.format('MMMM YYYY'); return timeMoment.format('MMMM YYYY');
} else if (view === 'agenda') { } else if (view === 'agenda') {
return `Agenda: ${startMoment.format('MMM D')} - ${endMoment.format('MMM D')}`; return 'Agenda';
} }
let startFormat = 'MMM D YYYY'; let startFormat = 'MMM D YYYY';
@@ -168,7 +168,7 @@ class CalendarHeader extends Component {
selectedView={view} selectedView={view}
onPress={this.onViewChange} onPress={this.onViewChange}
> >
{translate('Month')} Month
</ViewMenuItem> </ViewMenuItem>
} }
@@ -177,7 +177,7 @@ class CalendarHeader extends Component {
selectedView={view} selectedView={view}
onPress={this.onViewChange} onPress={this.onViewChange}
> >
{translate('Week')} Week
</ViewMenuItem> </ViewMenuItem>
<ViewMenuItem <ViewMenuItem
@@ -185,7 +185,7 @@ class CalendarHeader extends Component {
selectedView={view} selectedView={view}
onPress={this.onViewChange} onPress={this.onViewChange}
> >
{translate('Forecast')} Forecast
</ViewMenuItem> </ViewMenuItem>
<ViewMenuItem <ViewMenuItem
@@ -1,3 +1,4 @@
import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@@ -13,16 +14,19 @@ function createMapStateToProps() {
createDimensionsSelector(), createDimensionsSelector(),
createUISettingsSelector(), createUISettingsSelector(),
(calendar, dimensions, uiSettings) => { (calendar, dimensions, uiSettings) => {
return { const result = _.pick(calendar, [
isFetching: calendar.isFetching, 'isFetching',
view: calendar.view, 'view',
time: calendar.time, 'time',
start: calendar.start, 'start',
end: calendar.end, 'end'
isSmallScreen: dimensions.isSmallScreen, ]);
collapseViewButtons: dimensions.isLargeScreen,
longDateFormat: uiSettings.longDateFormat result.isSmallScreen = dimensions.isSmallScreen;
}; result.collapseViewButtons = dimensions.isLargeScreen;
result.longDateFormat = uiSettings.longDateFormat;
return result;
} }
); );
} }
+10 -24
View File
@@ -1,7 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import LegendIconItem from './LegendIconItem'; import LegendIconItem from './LegendIconItem';
import LegendItem from './LegendItem'; import LegendItem from './LegendItem';
import styles from './Legend.css'; import styles from './Legend.css';
@@ -17,10 +16,10 @@ function Legend(props) {
if (showCutoffUnmetIcon) { if (showCutoffUnmetIcon) {
iconsToShow.push( iconsToShow.push(
<LegendIconItem <LegendIconItem
name={translate('CutoffUnmet')} name="Cutoff Not Met"
icon={icons.MOVIE_FILE} icon={icons.MOVIE_FILE}
kind={kinds.WARNING} kind={kinds.WARNING}
tooltip={translate('QualityOrLangCutoffHasNotBeenMet')} tooltip="Quality or language cutoff has not been met"
/> />
); );
} }
@@ -29,45 +28,32 @@ function Legend(props) {
<div className={styles.legend}> <div className={styles.legend}>
<div> <div>
<LegendItem <LegendItem
style='ended' status="unreleased"
name={translate('DownloadedAndMonitored')} tooltip="Movie hasn't released yet"
colorImpairedMode={colorImpairedMode} colorImpairedMode={colorImpairedMode}
/> />
<LegendItem <LegendItem
style='availNotMonitored' status="unmonitored"
name={translate('DownloadedButNotMonitored')} tooltip="Movie is unmonitored"
colorImpairedMode={colorImpairedMode} colorImpairedMode={colorImpairedMode}
/> />
</div> </div>
<div> <div>
<LegendItem <LegendItem
style='missingMonitored' status="downloading"
name={translate('MissingMonitoredAndConsideredAvailable')} tooltip="Movie is currently downloading"
colorImpairedMode={colorImpairedMode} colorImpairedMode={colorImpairedMode}
/> />
<LegendItem <LegendItem
style='missingUnmonitored' status="downloaded"
name={translate('MissingNotMonitored')} tooltip="Movie was downloaded and sorted"
colorImpairedMode={colorImpairedMode} colorImpairedMode={colorImpairedMode}
/> />
</div> </div>
<div>
<LegendItem
style='queue'
name={translate('Queued')}
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
style='continuing'
name={translate('Unreleased')}
colorImpairedMode={colorImpairedMode}
/>
</div>
{ {
iconsToShow.length > 0 && iconsToShow.length > 0 &&
<div> <div>
@@ -1,5 +1,8 @@
.legendIconItem { .legendIconItem {
margin-left: 6px; margin: 3px 0;
margin-right: 6px;
width: 150px;
cursor: default;
} }
.icon { .icon {
+21 -62
View File
@@ -1,74 +1,33 @@
.legendItemContainer {
margin-right: 5px;
width: 220px;
}
.legendItem { .legendItem {
display: inline-flex; margin: 3px 0;
margin-top: -1px; margin-right: 6px;
vertical-align: middle; padding-left: 5px;
line-height: 16px; width: 150px;
border-left-width: 4px;
border-left-style: solid;
cursor: default;
} }
.legendItemColor { /*
margin-right: 8px; * Status
width: 30px; */
height: 16px;
border-radius: 4px; .downloaded {
composes: downloaded from '~Calendar/Events/CalendarEvent.css';
} }
.queue { .downloading {
composes: legendItemColor; composes: downloading from '~Calendar/Events/CalendarEvent.css';
background-color: $queueColor;
} }
.continuing { .unmonitored {
composes: legendItemColor; composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
background-color: $primaryColor;
} }
.availNotMonitored { .missing {
composes: legendItemColor; composes: missing from '~Calendar/Events/CalendarEvent.css';
background-color: $darkGray;
} }
.ended { .unreleased {
composes: legendItemColor; composes: unreleased from '~Calendar/Events/CalendarEvent.css';
background-color: $successColor;
}
.missingMonitored {
composes: legendItemColor;
background-color: $dangerColor;
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, color($dangerColor shade(5%)), color($dangerColor shade(5%)) 5px, color($dangerColor shade(15%)) 5px, color($dangerColor shade(15%)) 10px);
}
}
.missingUnmonitored {
composes: legendItemColor;
background-color: $warningColor;
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, $warningColor, $warningColor 5px, color($warningColor tint(15%)) 5px, color($warningColor tint(15%)) 10px);
}
}
.missingMonitoredColorImpaired {
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
.missingUnmonitoredColorImpaired {
background: repeating-linear-gradient(45deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
.legendItemText {
display: inline-block;
} }
+15 -14
View File
@@ -1,34 +1,35 @@
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import titleCase from 'Utilities/String/titleCase';
import styles from './LegendItem.css'; import styles from './LegendItem.css';
function LegendItem(props) { function LegendItem(props) {
const { const {
name, name,
style, status,
tooltip,
colorImpairedMode colorImpairedMode
} = props; } = props;
return ( return (
<div className={styles.legendItemContainer}> <div
<div className={classNames(
className={classNames( styles.legendItem,
styles.legendItem, styles[status],
styles[style], colorImpairedMode && 'colorImpaired'
colorImpairedMode && 'colorImpaired' )}
)} title={tooltip}
/> >
<div className={classNames(styles.legendItemText, colorImpairedMode && styles[`${style}ColorImpaired`])}> {name ? name : titleCase(status)}
{name}
</div>
</div> </div>
); );
} }
LegendItem.propTypes = { LegendItem.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string,
style: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
tooltip: PropTypes.string.isRequired,
colorImpairedMode: PropTypes.bool.isRequired colorImpairedMode: PropTypes.bool.isRequired
}; };
@@ -107,7 +107,7 @@ class CalendarOptionsModalContent extends Component {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{translate('CalendarOptions')} Calendar Options
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
+23
View File
@@ -0,0 +1,23 @@
function getStatusStyle(hasFile, downloading, isAvailable, isMonitored) {
if (hasFile) {
return 'downloaded';
}
if (downloading) {
return 'downloading';
}
if (!isMonitored) {
return 'unmonitored';
}
if (isAvailable && !hasFile) {
return 'missing';
}
return 'unreleased';
}
export default getStatusStyle;
@@ -22,7 +22,7 @@ function getUrls(state) {
tags tags
} = state; } = state;
let icalUrl = `${window.location.host}${window.Radarr.urlBase}/feed/v3/calendar/Radarr.ics?`; let icalUrl = `${window.location.host}${window.Radarr.urlBase}/feed/calendar/Radarr.ics?`;
if (unmonitored) { if (unmonitored) {
icalUrl += 'unmonitored=true&'; icalUrl += 'unmonitored=true&';
@@ -109,7 +109,7 @@ class CalendarLinkModalContent extends Component {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{translate('RadarrCalendarFeed')} Radarr Calendar Feed
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
+1 -1
View File
@@ -1,7 +1,7 @@
export const APPLICATION_UPDATE = 'ApplicationUpdate'; export const APPLICATION_UPDATE = 'ApplicationUpdate';
export const BACKUP = 'Backup'; export const BACKUP = 'Backup';
export const REFRESH_MONITORED_DOWNLOADS = 'RefreshMonitoredDownloads'; export const REFRESH_MONITORED_DOWNLOADS = 'RefreshMonitoredDownloads';
export const CLEAR_BLOCKLIST = 'ClearBlocklist'; export const CLEAR_BLACKLIST = 'ClearBlacklist';
export const CLEAR_LOGS = 'ClearLog'; export const CLEAR_LOGS = 'ClearLog';
export const CUTOFF_UNMET_MOVIES_SEARCH = 'CutoffUnmetMoviesSearch'; export const CUTOFF_UNMET_MOVIES_SEARCH = 'CutoffUnmetMoviesSearch';
export const DELETE_LOG_FILES = 'DeleteLogFiles'; export const DELETE_LOG_FILES = 'DeleteLogFiles';
@@ -129,9 +129,7 @@ class FileBrowserModalContent extends Component {
className={styles.mappedDrivesWarning} className={styles.mappedDrivesWarning}
kind={kinds.WARNING} kind={kinds.WARNING}
> >
<Link to="https://wiki.servarr.com/radarr/faq#why-cant-radarr-see-my-files-on-a-remote-server"> Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://github.com/Radarr/Radarr/wiki/FAQ">FAQ</Link> for more information.
{translate('MappedDrivesRunningAsService')}
</Link> .
</Alert> </Alert>
} }
@@ -5,17 +5,16 @@ import SelectInput from 'Components/Form/SelectInput';
import TextInput from 'Components/Form/TextInput'; import TextInput from 'Components/Form/TextInput';
import { IN_LAST, IN_NEXT, NOT_IN_LAST, NOT_IN_NEXT } from 'Helpers/Props/filterTypes'; import { IN_LAST, IN_NEXT, NOT_IN_LAST, NOT_IN_NEXT } from 'Helpers/Props/filterTypes';
import isString from 'Utilities/String/isString'; import isString from 'Utilities/String/isString';
import translate from 'Utilities/String/translate';
import { NAME } from './FilterBuilderRowValue'; import { NAME } from './FilterBuilderRowValue';
import styles from './DateFilterBuilderRowValue.css'; import styles from './DateFilterBuilderRowValue.css';
const timeOptions = [ const timeOptions = [
{ key: 'seconds', value: translate('Seconds') }, { key: 'seconds', value: 'seconds' },
{ key: 'minutes', value: translate('Minutes') }, { key: 'minutes', value: 'minutes' },
{ key: 'hours', value: translate('Hours') }, { key: 'hours', value: 'hours' },
{ key: 'days', value: translate('Days') }, { key: 'days', value: 'days' },
{ key: 'weeks', value: translate('Weeks') }, { key: 'weeks', value: 'weeks' },
{ key: 'months', value: translate('Months') } { key: 'months', value: 'months' }
]; ];
function isInFilter(filterType) { function isInFilter(filterType) {
@@ -8,11 +8,10 @@ import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector'; import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
import ImportListFilterBuilderRowValueConnector from './ImportListFilterBuilderRowValueConnector'; import ImportListFilterBuilderRowValueConnector from './ImportListFilterBuilderRowValueConnector';
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector'; import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
import MinimumAvailabilityFilterBuilderRowValue from './MinimumAvailabilityFilterBuilderRowValue'; import MovieStatusFilterBuilderRowValue from './MovieStatusFilterBuilderRowValue';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector'; import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector'; import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
import ReleaseStatusFilterBuilderRowValue from './ReleaseStatusFilterBuilderRowValue';
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector'; import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
import styles from './FilterBuilderRow.css'; import styles from './FilterBuilderRow.css';
@@ -70,11 +69,8 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.QUALITY_PROFILE: case filterBuilderValueTypes.QUALITY_PROFILE:
return QualityProfileFilterBuilderRowValueConnector; return QualityProfileFilterBuilderRowValueConnector;
case filterBuilderValueTypes.RELEASE_STATUS: case filterBuilderValueTypes.MOVIE_STATUS:
return ReleaseStatusFilterBuilderRowValue; return MovieStatusFilterBuilderRowValue;
case filterBuilderValueTypes.MINIMUM_AVAILABILITY:
return MinimumAvailabilityFilterBuilderRowValue;
case filterBuilderValueTypes.TAG: case filterBuilderValueTypes.TAG:
return TagFilterBuilderRowValueConnector; return TagFilterBuilderRowValueConnector;
@@ -16,7 +16,7 @@ function createTagListSelector() {
(selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER || (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ||
selectedFilterBuilderProp.type === filterBuilderTypes.STRING) && selectedFilterBuilderProp.type === filterBuilderTypes.STRING) &&
filterType !== filterTypes.EQUAL && filterType !== filterTypes.EQUAL &&
filterType !== filterTypes.NOT_EQUAL || filterType !== filterBuilderTypes.NOT_EQUAL ||
!selectedFilterBuilderProp.optionsSelector !selectedFilterBuilderProp.optionsSelector
) { ) {
return []; return [];
@@ -1,20 +0,0 @@
import React from 'react';
import translate from 'Utilities/String/translate';
import FilterBuilderRowValue from './FilterBuilderRowValue';
const protocols = [
{ id: 'announced', name: translate('Announced') },
{ id: 'inCinemas', name: translate('InCinemas') },
{ id: 'released', name: translate('Released') }
];
function MinimumAvailabilityFilterBuilderRowValue(props) {
return (
<FilterBuilderRowValue
tagList={protocols}
{...props}
/>
);
}
export default MinimumAvailabilityFilterBuilderRowValue;
@@ -0,0 +1,21 @@
import React from 'react';
import FilterBuilderRowValue from './FilterBuilderRowValue';
const protocols = [
{ id: 'tba', name: 'TBA' },
{ id: 'announced', name: 'Announced' },
{ id: 'inCinemas', name: 'In Cinemas' },
{ id: 'released', name: 'Released' },
{ id: 'deleted', name: 'Deleted' }
];
function MovieStatusFilterBuilderRowValue(props) {
return (
<FilterBuilderRowValue
tagList={protocols}
{...props}
/>
);
}
export default MovieStatusFilterBuilderRowValue;
@@ -1,22 +0,0 @@
import React from 'react';
import translate from 'Utilities/String/translate';
import FilterBuilderRowValue from './FilterBuilderRowValue';
const protocols = [
{ id: 'tba', name: 'TBA' },
{ id: 'announced', name: translate('Announced') },
{ id: 'inCinemas', name: translate('InCinemas') },
{ id: 'released', name: translate('Released') },
{ id: 'deleted', name: translate('Deleted') }
];
function ReleaseStatusFilterBuilderRowValue(props) {
return (
<FilterBuilderRowValue
tagList={protocols}
{...props}
/>
);
}
export default ReleaseStatusFilterBuilderRowValue;
@@ -1,13 +1,12 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import translate from 'Utilities/String/translate';
import SelectInput from './SelectInput'; import SelectInput from './SelectInput';
const availabilityOptions = [ const availabilityOptions = [
{ key: 'announced', value: translate('Announced') }, { key: 'announced', value: 'Announced' },
{ key: 'inCinemas', value: translate('InCinemas') }, { key: 'inCinemas', value: 'In Cinemas' },
{ key: 'released', value: translate('Released') }, { key: 'released', value: 'Released' },
{ key: 'preDB', value: translate('PreDB') } { key: 'preDB', value: 'PreDB' }
]; ];
function AvailabilitySelectInput(props) { function AvailabilitySelectInput(props) {
@@ -2,22 +2,34 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { clearOptions, defaultState, fetchOptions } from 'Store/Actions/providerOptionActions'; import { clearOptions, fetchOptions } from 'Store/Actions/providerOptionActions';
import DeviceInput from './DeviceInput'; import DeviceInput from './DeviceInput';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state, { value }) => value, (state, { value }) => value,
(state) => state.providerOptions.devices || defaultState, (state, { name }) => name,
(value, devices) => { (state) => state.providerOptions,
(value, name, devices) => {
const {
isFetching,
isPopulated,
error,
items
} = devices;
return { return {
...devices, isFetching,
isPopulated,
error,
items: items[name] || [],
selectedDevices: value.map((valueDevice) => { selectedDevices: value.map((valueDevice) => {
const sectionItems = items[name] || [];
// Disable equality ESLint rule so we don't need to worry about // Disable equality ESLint rule so we don't need to worry about
// a type mismatch between the value items and the device ID. // a type mismatch between the value items and the device ID.
// eslint-disable-next-line eqeqeq // eslint-disable-next-line eqeqeq
const device = devices.items.find((d) => d.id == valueDevice); const device = sectionItems.find((d) => d.id == valueDevice);
if (device) { if (device) {
return { return {
@@ -51,7 +63,7 @@ class DeviceInputConnector extends Component {
} }
componentWillUnmount = () => { componentWillUnmount = () => {
this.props.dispatchClearOptions({ section: 'devices' }); this.props.dispatchClearOptions();
} }
// //
@@ -61,12 +73,14 @@ class DeviceInputConnector extends Component {
const { const {
provider, provider,
providerData, providerData,
dispatchFetchOptions dispatchFetchOptions,
requestAction,
name
} = this.props; } = this.props;
dispatchFetchOptions({ dispatchFetchOptions({
section: 'devices', action: requestAction,
action: 'getDevices', itemSection: name,
provider, provider,
providerData providerData
}); });
@@ -95,6 +109,7 @@ class DeviceInputConnector extends Component {
DeviceInputConnector.propTypes = { DeviceInputConnector.propTypes = {
provider: PropTypes.string.isRequired, provider: PropTypes.string.isRequired,
providerData: PropTypes.object.isRequired, providerData: PropTypes.object.isRequired,
requestAction: PropTypes.string.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
dispatchFetchOptions: PropTypes.func.isRequired, dispatchFetchOptions: PropTypes.func.isRequired,

Some files were not shown because too many files have changed in this diff Show More