Merge branch 'master' into Vimeo-issue-16717
This commit is contained in:
commit
d31329aee7
6
.github/ISSUE_TEMPLATE.md
vendored
6
.github/ISSUE_TEMPLATE.md
vendored
@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Make sure you are using the *latest* version: run `youtube-dl --version` and ensure your version is *2018.07.04*. If it's not, read [this FAQ entry](https://github.com/rg3/youtube-dl/blob/master/README.md#how-do-i-update-youtube-dl) and update. Issues with outdated version will be rejected.
|
### Make sure you are using the *latest* version: run `youtube-dl --version` and ensure your version is *2018.07.10*. If it's not, read [this FAQ entry](https://github.com/rg3/youtube-dl/blob/master/README.md#how-do-i-update-youtube-dl) and update. Issues with outdated version will be rejected.
|
||||||
- [ ] I've **verified** and **I assure** that I'm running youtube-dl **2018.07.04**
|
- [ ] I've **verified** and **I assure** that I'm running youtube-dl **2018.07.10**
|
||||||
|
|
||||||
### Before submitting an *issue* make sure you have:
|
### Before submitting an *issue* make sure you have:
|
||||||
- [ ] At least skimmed through the [README](https://github.com/rg3/youtube-dl/blob/master/README.md), **most notably** the [FAQ](https://github.com/rg3/youtube-dl#faq) and [BUGS](https://github.com/rg3/youtube-dl#bugs) sections
|
- [ ] At least skimmed through the [README](https://github.com/rg3/youtube-dl/blob/master/README.md), **most notably** the [FAQ](https://github.com/rg3/youtube-dl#faq) and [BUGS](https://github.com/rg3/youtube-dl#bugs) sections
|
||||||
@ -36,7 +36,7 @@ Add the `-v` flag to **your command line** you run youtube-dl with (`youtube-dl
|
|||||||
[debug] User config: []
|
[debug] User config: []
|
||||||
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
|
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
|
||||||
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
||||||
[debug] youtube-dl version 2018.07.04
|
[debug] youtube-dl version 2018.07.10
|
||||||
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
|
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
|
||||||
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
||||||
[debug] Proxy map: {}
|
[debug] Proxy map: {}
|
||||||
|
17
ChangeLog
17
ChangeLog
@ -1,3 +1,20 @@
|
|||||||
|
version 2018.07.10
|
||||||
|
|
||||||
|
Core
|
||||||
|
* [utils] Share JSON-LD regular expression
|
||||||
|
* [downloader/dash] Improve error handling (#16927)
|
||||||
|
|
||||||
|
Extractors
|
||||||
|
+ [nrktv] Add support for new season and serie URL schema
|
||||||
|
+ [nrktv] Add support for new episode URL schema (#16909)
|
||||||
|
+ [frontendmasters] Add support for frontendmasters.com (#3661, #16328)
|
||||||
|
* [funk] Fix extraction (#16918)
|
||||||
|
* [watchbox] Fix extraction (#16904)
|
||||||
|
* [dplayit] Sort formats
|
||||||
|
* [dplayit] Fix extraction (#16901)
|
||||||
|
* [youtube] Improve login error handling (#13822)
|
||||||
|
|
||||||
|
|
||||||
version 2018.07.04
|
version 2018.07.04
|
||||||
|
|
||||||
Core
|
Core
|
||||||
|
@ -302,6 +302,9 @@
|
|||||||
- **Freesound**
|
- **Freesound**
|
||||||
- **freespeech.org**
|
- **freespeech.org**
|
||||||
- **FreshLive**
|
- **FreshLive**
|
||||||
|
- **FrontendMasters**
|
||||||
|
- **FrontendMastersCourse**
|
||||||
|
- **FrontendMastersLesson**
|
||||||
- **Funimation**
|
- **Funimation**
|
||||||
- **FunkChannel**
|
- **FunkChannel**
|
||||||
- **FunkMix**
|
- **FunkMix**
|
||||||
@ -589,7 +592,9 @@
|
|||||||
- **NRKSkole**: NRK Skole
|
- **NRKSkole**: NRK Skole
|
||||||
- **NRKTV**: NRK TV and NRK Radio
|
- **NRKTV**: NRK TV and NRK Radio
|
||||||
- **NRKTVDirekte**: NRK TV Direkte and NRK Radio Direkte
|
- **NRKTVDirekte**: NRK TV Direkte and NRK Radio Direkte
|
||||||
|
- **NRKTVEpisode**
|
||||||
- **NRKTVEpisodes**
|
- **NRKTVEpisodes**
|
||||||
|
- **NRKTVSeason**
|
||||||
- **NRKTVSeries**
|
- **NRKTVSeries**
|
||||||
- **ntv.ru**
|
- **ntv.ru**
|
||||||
- **Nuvid**
|
- **Nuvid**
|
||||||
|
@ -2,7 +2,10 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from .fragment import FragmentFD
|
from .fragment import FragmentFD
|
||||||
from ..compat import compat_urllib_error
|
from ..compat import compat_urllib_error
|
||||||
from ..utils import urljoin
|
from ..utils import (
|
||||||
|
DownloadError,
|
||||||
|
urljoin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DashSegmentsFD(FragmentFD):
|
class DashSegmentsFD(FragmentFD):
|
||||||
@ -57,6 +60,14 @@ class DashSegmentsFD(FragmentFD):
|
|||||||
count += 1
|
count += 1
|
||||||
if count <= fragment_retries:
|
if count <= fragment_retries:
|
||||||
self.report_retry_fragment(err, frag_index, count, fragment_retries)
|
self.report_retry_fragment(err, frag_index, count, fragment_retries)
|
||||||
|
except DownloadError:
|
||||||
|
# Don't retry fragment if error occurred during HTTP downloading
|
||||||
|
# itself since it has own retry settings
|
||||||
|
if not fatal:
|
||||||
|
self.report_skip_fragment(frag_index)
|
||||||
|
break
|
||||||
|
raise
|
||||||
|
|
||||||
if count > fragment_retries:
|
if count > fragment_retries:
|
||||||
if not fatal:
|
if not fatal:
|
||||||
self.report_skip_fragment(frag_index)
|
self.report_skip_fragment(frag_index)
|
||||||
|
@ -52,6 +52,7 @@ from ..utils import (
|
|||||||
GeoUtils,
|
GeoUtils,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
|
JSON_LD_RE,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
orderedSet,
|
orderedSet,
|
||||||
parse_codecs,
|
parse_codecs,
|
||||||
@ -1149,8 +1150,7 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
def _search_json_ld(self, html, video_id, expected_type=None, **kwargs):
|
def _search_json_ld(self, html, video_id, expected_type=None, **kwargs):
|
||||||
json_ld = self._search_regex(
|
json_ld = self._search_regex(
|
||||||
r'(?s)<script[^>]+type=(["\'])application/ld\+json\1[^>]*>(?P<json_ld>.+?)</script>',
|
JSON_LD_RE, html, 'JSON-LD', group='json_ld', **kwargs)
|
||||||
html, 'JSON-LD', group='json_ld', **kwargs)
|
|
||||||
default = kwargs.get('default', NO_DEFAULT)
|
default = kwargs.get('default', NO_DEFAULT)
|
||||||
if not json_ld:
|
if not json_ld:
|
||||||
return default if default is not NO_DEFAULT else {}
|
return default if default is not NO_DEFAULT else {}
|
||||||
|
@ -768,7 +768,9 @@ from .nrk import (
|
|||||||
NRKSkoleIE,
|
NRKSkoleIE,
|
||||||
NRKTVIE,
|
NRKTVIE,
|
||||||
NRKTVDirekteIE,
|
NRKTVDirekteIE,
|
||||||
|
NRKTVEpisodeIE,
|
||||||
NRKTVEpisodesIE,
|
NRKTVEpisodesIE,
|
||||||
|
NRKTVSeasonIE,
|
||||||
NRKTVSeriesIE,
|
NRKTVSeriesIE,
|
||||||
)
|
)
|
||||||
from .ntvde import NTVDeIE
|
from .ntvde import NTVDeIE
|
||||||
|
@ -4,12 +4,18 @@ from __future__ import unicode_literals
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_urllib_parse_unquote
|
from ..compat import (
|
||||||
|
compat_str,
|
||||||
|
compat_urllib_parse_unquote,
|
||||||
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
JSON_LD_RE,
|
||||||
|
NO_DEFAULT,
|
||||||
parse_age_limit,
|
parse_age_limit,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
|
try_get,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -359,6 +365,182 @@ class NRKTVIE(NRKBaseIE):
|
|||||||
}]
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
class NRKTVEpisodeIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://tv\.nrk\.no/serie/(?P<id>[^/]+/sesong/\d+/episode/\d+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://tv.nrk.no/serie/backstage/sesong/1/episode/8',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'MSUI14000816AA',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Backstage 8:30',
|
||||||
|
'description': 'md5:de6ca5d5a2d56849e4021f2bf2850df4',
|
||||||
|
'duration': 1320,
|
||||||
|
'series': 'Backstage',
|
||||||
|
'season_number': 1,
|
||||||
|
'episode_number': 8,
|
||||||
|
'episode': '8:30',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
|
||||||
|
nrk_id = self._parse_json(
|
||||||
|
self._search_regex(JSON_LD_RE, webpage, 'JSON-LD', group='json_ld'),
|
||||||
|
display_id)['@id']
|
||||||
|
|
||||||
|
assert re.match(NRKTVIE._EPISODE_RE, nrk_id)
|
||||||
|
return self.url_result(
|
||||||
|
'nrk:%s' % nrk_id, ie=NRKIE.ie_key(), video_id=nrk_id)
|
||||||
|
|
||||||
|
|
||||||
|
class NRKTVSerieBaseIE(InfoExtractor):
|
||||||
|
def _extract_series(self, webpage, display_id, fatal=True):
|
||||||
|
config = self._parse_json(
|
||||||
|
self._search_regex(
|
||||||
|
r'({.+?})\s*,\s*"[^"]+"\s*\)\s*</script>', webpage, 'config',
|
||||||
|
default='{}' if not fatal else NO_DEFAULT),
|
||||||
|
display_id, fatal=False)
|
||||||
|
if not config:
|
||||||
|
return
|
||||||
|
return try_get(config, lambda x: x['series'], dict)
|
||||||
|
|
||||||
|
def _extract_episodes(self, season):
|
||||||
|
entries = []
|
||||||
|
if not isinstance(season, dict):
|
||||||
|
return entries
|
||||||
|
episodes = season.get('episodes')
|
||||||
|
if not isinstance(episodes, list):
|
||||||
|
return entries
|
||||||
|
for episode in episodes:
|
||||||
|
nrk_id = episode.get('prfId')
|
||||||
|
if not nrk_id or not isinstance(nrk_id, compat_str):
|
||||||
|
continue
|
||||||
|
entries.append(self.url_result(
|
||||||
|
'nrk:%s' % nrk_id, ie=NRKIE.ie_key(), video_id=nrk_id))
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
class NRKTVSeasonIE(NRKTVSerieBaseIE):
|
||||||
|
_VALID_URL = r'https?://tv\.nrk\.no/serie/[^/]+/sesong/(?P<id>\d+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://tv.nrk.no/serie/backstage/sesong/1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1',
|
||||||
|
'title': 'Sesong 1',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 30,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def suitable(cls, url):
|
||||||
|
return (False if NRKTVIE.suitable(url) or NRKTVEpisodeIE.suitable(url)
|
||||||
|
else super(NRKTVSeasonIE, cls).suitable(url))
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
|
||||||
|
series = self._extract_series(webpage, display_id)
|
||||||
|
|
||||||
|
season = next(
|
||||||
|
s for s in series['seasons']
|
||||||
|
if int(display_id) == s.get('seasonNumber'))
|
||||||
|
|
||||||
|
title = try_get(season, lambda x: x['titles']['title'], compat_str)
|
||||||
|
return self.playlist_result(
|
||||||
|
self._extract_episodes(season), display_id, title)
|
||||||
|
|
||||||
|
|
||||||
|
class NRKTVSeriesIE(NRKTVSerieBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:tv|radio)\.nrk(?:super)?\.no/serie/(?P<id>[^/]+)'
|
||||||
|
_ITEM_RE = r'(?:data-season=["\']|id=["\']season-)(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
# new layout
|
||||||
|
'url': 'https://tv.nrk.no/serie/backstage',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'backstage',
|
||||||
|
'title': 'Backstage',
|
||||||
|
'description': 'md5:c3ec3a35736fca0f9e1207b5511143d3',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 60,
|
||||||
|
}, {
|
||||||
|
# old layout
|
||||||
|
'url': 'https://tv.nrk.no/serie/groenn-glede',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'groenn-glede',
|
||||||
|
'title': 'Grønn glede',
|
||||||
|
'description': 'md5:7576e92ae7f65da6993cf90ee29e4608',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 9,
|
||||||
|
}, {
|
||||||
|
'url': 'http://tv.nrksuper.no/serie/labyrint',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'labyrint',
|
||||||
|
'title': 'Labyrint',
|
||||||
|
'description': 'md5:58afd450974c89e27d5a19212eee7115',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 3,
|
||||||
|
}, {
|
||||||
|
'url': 'https://tv.nrk.no/serie/broedrene-dal-og-spektralsteinene',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://tv.nrk.no/serie/saving-the-human-race',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://tv.nrk.no/serie/postmann-pat',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def suitable(cls, url):
|
||||||
|
return (
|
||||||
|
False if any(ie.suitable(url)
|
||||||
|
for ie in (NRKTVIE, NRKTVEpisodeIE, NRKTVSeasonIE))
|
||||||
|
else super(NRKTVSeriesIE, cls).suitable(url))
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
series_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, series_id)
|
||||||
|
|
||||||
|
# New layout (e.g. https://tv.nrk.no/serie/backstage)
|
||||||
|
series = self._extract_series(webpage, series_id, fatal=False)
|
||||||
|
if series:
|
||||||
|
title = try_get(series, lambda x: x['titles']['title'], compat_str)
|
||||||
|
description = try_get(
|
||||||
|
series, lambda x: x['titles']['subtitle'], compat_str)
|
||||||
|
entries = []
|
||||||
|
for season in series['seasons']:
|
||||||
|
entries.extend(self._extract_episodes(season))
|
||||||
|
return self.playlist_result(entries, series_id, title, description)
|
||||||
|
|
||||||
|
# Old layout (e.g. https://tv.nrk.no/serie/groenn-glede)
|
||||||
|
entries = [
|
||||||
|
self.url_result(
|
||||||
|
'https://tv.nrk.no/program/Episodes/{series}/{season}'.format(
|
||||||
|
series=series_id, season=season_id))
|
||||||
|
for season_id in re.findall(self._ITEM_RE, webpage)
|
||||||
|
]
|
||||||
|
|
||||||
|
title = self._html_search_meta(
|
||||||
|
'seriestitle', webpage,
|
||||||
|
'title', default=None) or self._og_search_title(
|
||||||
|
webpage, fatal=False)
|
||||||
|
|
||||||
|
description = self._html_search_meta(
|
||||||
|
'series_description', webpage,
|
||||||
|
'description', default=None) or self._og_search_description(webpage)
|
||||||
|
|
||||||
|
return self.playlist_result(entries, series_id, title, description)
|
||||||
|
|
||||||
|
|
||||||
class NRKTVDirekteIE(NRKTVIE):
|
class NRKTVDirekteIE(NRKTVIE):
|
||||||
IE_DESC = 'NRK TV Direkte and NRK Radio Direkte'
|
IE_DESC = 'NRK TV Direkte and NRK Radio Direkte'
|
||||||
_VALID_URL = r'https?://(?:tv|radio)\.nrk\.no/direkte/(?P<id>[^/?#&]+)'
|
_VALID_URL = r'https?://(?:tv|radio)\.nrk\.no/direkte/(?P<id>[^/?#&]+)'
|
||||||
@ -438,64 +620,6 @@ class NRKTVEpisodesIE(NRKPlaylistBaseIE):
|
|||||||
r'<h1>([^<]+)</h1>', webpage, 'title', fatal=False)
|
r'<h1>([^<]+)</h1>', webpage, 'title', fatal=False)
|
||||||
|
|
||||||
|
|
||||||
class NRKTVSeriesIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'https?://(?:tv|radio)\.nrk(?:super)?\.no/serie/(?P<id>[^/]+)'
|
|
||||||
_ITEM_RE = r'(?:data-season=["\']|id=["\']season-)(?P<id>\d+)'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://tv.nrk.no/serie/groenn-glede',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'groenn-glede',
|
|
||||||
'title': 'Grønn glede',
|
|
||||||
'description': 'md5:7576e92ae7f65da6993cf90ee29e4608',
|
|
||||||
},
|
|
||||||
'playlist_mincount': 9,
|
|
||||||
}, {
|
|
||||||
'url': 'http://tv.nrksuper.no/serie/labyrint',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'labyrint',
|
|
||||||
'title': 'Labyrint',
|
|
||||||
'description': 'md5:58afd450974c89e27d5a19212eee7115',
|
|
||||||
},
|
|
||||||
'playlist_mincount': 3,
|
|
||||||
}, {
|
|
||||||
'url': 'https://tv.nrk.no/serie/broedrene-dal-og-spektralsteinene',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://tv.nrk.no/serie/saving-the-human-race',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://tv.nrk.no/serie/postmann-pat',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def suitable(cls, url):
|
|
||||||
return False if NRKTVIE.suitable(url) else super(NRKTVSeriesIE, cls).suitable(url)
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
series_id = self._match_id(url)
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, series_id)
|
|
||||||
|
|
||||||
entries = [
|
|
||||||
self.url_result(
|
|
||||||
'https://tv.nrk.no/program/Episodes/{series}/{season}'.format(
|
|
||||||
series=series_id, season=season_id))
|
|
||||||
for season_id in re.findall(self._ITEM_RE, webpage)
|
|
||||||
]
|
|
||||||
|
|
||||||
title = self._html_search_meta(
|
|
||||||
'seriestitle', webpage,
|
|
||||||
'title', default=None) or self._og_search_title(
|
|
||||||
webpage, fatal=False)
|
|
||||||
|
|
||||||
description = self._html_search_meta(
|
|
||||||
'series_description', webpage,
|
|
||||||
'description', default=None) or self._og_search_description(webpage)
|
|
||||||
|
|
||||||
return self.playlist_result(entries, series_id, title, description)
|
|
||||||
|
|
||||||
|
|
||||||
class NRKSkoleIE(InfoExtractor):
|
class NRKSkoleIE(InfoExtractor):
|
||||||
IE_DESC = 'NRK Skole'
|
IE_DESC = 'NRK Skole'
|
||||||
_VALID_URL = r'https?://(?:www\.)?nrk\.no/skole/?\?.*\bmediaId=(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?nrk\.no/skole/?\?.*\bmediaId=(?P<id>\d+)'
|
||||||
|
@ -184,6 +184,7 @@ DATE_FORMATS_MONTH_FIRST.extend([
|
|||||||
])
|
])
|
||||||
|
|
||||||
PACKED_CODES_RE = r"}\('(.+)',(\d+),(\d+),'([^']+)'\.split\('\|'\)"
|
PACKED_CODES_RE = r"}\('(.+)',(\d+),(\d+),'([^']+)'\.split\('\|'\)"
|
||||||
|
JSON_LD_RE = r'(?is)<script[^>]+type=(["\'])application/ld\+json\1[^>]*>(?P<json_ld>.+?)</script>'
|
||||||
|
|
||||||
|
|
||||||
def preferredencoding():
|
def preferredencoding():
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
__version__ = '2018.07.04'
|
__version__ = '2018.07.10'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user